在渗透自己网站的时候发现了一个可疑的日志

2984 字
15 分钟
在渗透自己网站的时候发现了一个可疑的日志

注 事件 A —— 169.58.159.96 事件 B —— 103.46.186.148

先拉取日志#

Terminal window
# 1) 事件 A:WAF 上线前的扫描(10-12 那一整天在 access.log.1 里)
grep -E '^169\.58\.159\.96 ' /var/log/nginx/access.log.1 \
> attack-2026-10-12-169.58.159.96.access.log
# 2) 事件 B:同一个 IP 在 WAF 上线后的扫描
grep -E '^103\.46\.186\.148 ' /var/log/nginx/access.log.1 \
> attack-2026-10-12-103.46.186.148.public.access.log
# 3) 事件 B 的回源记录
grep -E '^103\.46\.186\.148 ' /var/log/nginx/access-internal.log \
> attack-2026-10-12-103.46.186.148.internal.access.log
# 4) 事件 B 的 WAF 审计记录(jsonl 属主是 ubuntu,但习惯上加 sudo)
sudo grep -E '"client": "103\.46\.186\.148"' \
/opt/waf-lab/logs/waf-2026-10-12.jsonl \
> attack-2026-10-12-103.46.186.148.waf.jsonl
  • 用的是 ^IP (行首 + 空格)锚定,不是裸子串。裸 grep 「1.2.3.4」 会误匹配到 referer、UA 甚至别的 IP 里恰好包含这串数字的情况。

  • nginx 的访问日志按天轮转:当天在 access.log,前一天在 access.log.1, 更早的 .N.gz。10-12 全天的记录在 access.log.1,不在 access.log。 找历史事件先看时间,再选文件,别只盯 access.log。


时间线(systemd journal + 配置 mtime 实测)#

这条时间线是判断「为什么事件 A 在 WAF 日志里一条都没有」的关键

  • 2026-10-12 01:06:52 —— WAF 服务第一次起来,audit 模式。来源:journalctl -u waf-lab 的启动横幅「模式:审计模式(audit,只记录不拦截)」。此时它只是个跑在本机 127.0.0.1:8888 的进程,公网流量完全不经过它。

  • 2026-10-12 01:54:23 ~ 01:54:56 —— 事件 A(169.58.159.96,49 条)。此时公网 server 还没有 proxy_pass,请求直连静态文件。

  • 2026-10-12 09:39:35 —— 公网 server 配置写入 proxy_pass http://127.0.0.1:8888。来源:ls --time-style=full-iso /etc/nginx/sites-available/lyh-blogs 显示 mtime 为 09:39:35。这才是公网切到 WAF 的时刻,同一秒 WAF 也重启了一次(仍是 audit)。

  • 2026-10-12 10:42:31 —— WAF 重启,仍为 audit。

  • 2026-10-12 11:30:29 —— WAF 重启,切到 block。从这一刻起命中阈值的请求会被真的 403。

  • 2026-10-12 11:44:50 —— 重启(block)。

  • 2026-10-12 11:46:05 —— 重启,block,这就是当前一直跑着的那个进程(systemctl show waf-lab -p ExecMainStartTimestamp = Mon 2026-10-12 11:46:05)。

  • 2026-10-12 14:43:32 ~ 14:44:18 —— 事件 B(103.46.186.148,49 条),block 模式下运行。

注意事件 B 的审计记录里 would_block 全是 false、action 里有 5 条是 block。这两个字段要一起看:would_block 只在 audit 模式把「本该拦」降级成「只记录」时才为 true(见 waf/engine.py 第 583~586 行)。所以 action=block + would_block=false的含义是真的拦了,和公网日志里的 5 个 403 对得上。


看看拉到的日志有没有问题#

  • attack-2026-10-12-169.58.159.96.access.log —— 事件 A 的公网访问日志,49 行,来自服务器 /var/log/nginx/access.log.1

  • attack-2026-10-12-103.46.186.148.public.access.log —— 事件 B 的公网访问日志,49 行,同一个文件

  • attack-2026-10-12-103.46.186.148.internal.access.log —— 事件 B 的内部回源日志,42 行,来自 /var/log/nginx/access-internal.log; 末列 peer=127.0.0.1 是回源对端,用来证明「这条请求真的进到了静态文件那层」

  • attack-2026-10-12-103.46.186.148.waf.jsonl —— 事件 B 在 WAF 侧的审计记录,47 条,来自 /opt/waf-lab/logs/waf-2026-10-12.jsonl

为什么事件 B 有三个不同条数(公网 49 / WAF 审计 47 / 内部 42),链路上一环扣一环:

  • 公网 49 —— nginx 记下它收到的全部请求

  • WAF 审计 47 —— 其中 2 条被公网 nginx 在请求行解析阶段就返回 400 掐掉了, 压根没走到 proxy_pass,所以 WAF 看不到(49 − 2 = 47)

  • 内部 42 —— 47 条里有 5 条被 WAF 判为 block 并直接返回 403,不会回源 (47 − 5 = 42)


排查时发现可疑的ip,不是我的机器却用一些可疑的方式访问我的blog#

先看这个日志

169.58.159.96 - - [12/Oct/2026:01:54:23 +0800] "POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1" 400 166 "-" "-"

发现这个ip:169.58.159.96,不是我的机子,有问题。

原始文件:attack-2026-10-12-169.58.159.96.access.log(49 行,01:54:23 ~ 01:54:56,约 33 秒,平均间隔 0.7 秒)

1. 01:54:23 +0800 POST 400 /cgi-bin/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/.%2e/bin/sh
2. 01:54:23 +0800 POST 400 /cgi-bin/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/%%32%65%%32%65/bin/sh
3. 01:54:24 +0800 POST 404 /hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input
4. 01:54:25 +0800 POST 405 /?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input
5. 01:54:25 +0800 POST 404 /index.php?%25ADd+allow_url_include%3D1+%25ADd+auto_prepend_file%3Dphp://input
6. 01:54:25 +0800 POST 404 /test.hello?%25ADd+allow_url_include%3D1+%25ADd+auto_prepend_file%3Dphp://input
7. 01:54:26 +0800 POST 404 /index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input
8. 01:54:27 +0800 GET 404 /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
9. 01:54:27 +0800 GET 404 /vendor/phpunit/phpunit/Util/PHP/eval-stdin.php
10. 01:54:28 +0800 GET 404 /vendor/phpunit/src/Util/PHP/eval-stdin.php
11. 01:54:28 +0800 GET 404 /vendor/phpunit/Util/PHP/eval-stdin.php
12. 01:54:29 +0800 GET 404 /vendor/phpunit/phpunit/LICENSE/eval-stdin.php
13. 01:54:30 +0800 GET 404 /vendor/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
14. 01:54:31 +0800 GET 404 /phpunit/phpunit/src/Util/PHP/eval-stdin.php
15. 01:54:31 +0800 GET 404 /phpunit/phpunit/Util/PHP/eval-stdin.php
16. 01:54:32 +0800 GET 404 /phpunit/src/Util/PHP/eval-stdin.php
17. 01:54:33 +0800 GET 404 /phpunit/Util/PHP/eval-stdin.php
18. 01:54:33 +0800 GET 404 /lib/phpunit/phpunit/src/Util/PHP/eval-stdin.php
19. 01:54:34 +0800 GET 404 /lib/phpunit/phpunit/Util/PHP/eval-stdin.php
20. 01:54:35 +0800 GET 404 /lib/phpunit/src/Util/PHP/eval-stdin.php
21. 01:54:36 +0800 GET 404 /lib/phpunit/Util/PHP/eval-stdin.php
22. 01:54:37 +0800 GET 404 /lib/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
23. 01:54:38 +0800 GET 404 /laravel/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
24. 01:54:38 +0800 GET 404 /www/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
25. 01:54:39 +0800 GET 404 /ws/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
26. 01:54:40 +0800 GET 404 /yii/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
27. 01:54:40 +0800 GET 404 /zend/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
28. 01:54:41 +0800 GET 404 /ws/ec/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
29. 01:54:42 +0800 GET 404 /V2/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
30. 01:54:42 +0800 GET 404 /tests/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
31. 01:54:43 +0800 GET 404 /test/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
32. 01:54:44 +0800 GET 404 /testing/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
33. 01:54:45 +0800 GET 404 /api/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
34. 01:54:45 +0800 GET 404 /demo/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
35. 01:54:46 +0800 GET 404 /cms/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
36. 01:54:47 +0800 GET 404 /crm/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
37. 01:54:47 +0800 GET 404 /admin/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
38. 01:54:48 +0800 GET 404 /backup/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
39. 01:54:49 +0800 GET 404 /blog/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
40. 01:54:50 +0800 GET 404 /workspace/drupal/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
41. 01:54:51 +0800 GET 404 /panel/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
42. 01:54:51 +0800 GET 404 /public/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
43. 01:54:52 +0800 GET 404 /apps/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
44. 01:54:53 +0800 GET 404 /app/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php
45. 01:54:53 +0800 GET 404 /index.php?s=/index/\x5Cthink\x5Capp/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=Hello
46. 01:54:54 +0800 GET 404 /public/index.php?s=/index/\x5Cthink\x5Capp/invokefunction&function=call_user_func_array&vars[0]=md5&vars[1][]=Hello
47. 01:54:55 +0800 GET 404 /index.php?lang=../../../../../../../../usr/local/lib/php/pearcmd&+config-create+/&/<?echo(md5(\x22hi\x22));?>+/tmp/index1.php
48. 01:54:55 +0800 GET 404 /index.php?lang=../../../../../../../../tmp/index1
49. 01:54:56 +0800 GET 404 /containers/json

按载荷族归类(同一批 49 条):

  • PHPUnit eval-stdin.php(CVE-2017-9841) —— 37 条
  • PHP-CGI 参数注入 —— 5 条
  • cgi-bin 路径穿越(CVE-2021-41773) —— 2 条
  • ThinkPHP invokefunction —— 2 条
  • PHP pearcmd config-create —— 2 条
  • Docker API 探测 —— 1 条

小查一手发现

归属查询命令:

Terminal window
dig +short -x 169.58.159.96 # 反向 DNS
whois -h whois.ripe.net 169.58.159.96 # 欧洲地址查 RIPE(直接问 RIPE,别用默认 whois 服务器,
# 默认可能只返回上游的大块 ERX 记录)
whois 103.46.186.148 # 亚太地址走 APNIC
反向 DNS : vmi3629269.contaboserver.net
RIPE whois: netname TT-20260630
descr Contabo GmbH
country DE(德国)
abuse-c MH12453-RIPE

结论:不是”某台设备”,而是一台租来的云服务器。 vmi3629269.contaboserver.net 是 Contabo(德国主机商)的 VPS 命名规则,说明攻击者租用了一台 VPS 专门跑扫描器。


ta要干嘛(49 条,2026-10-12 01:54:23 ~ 01:54:50,UA libredtail-http)#

  • CVE-2021-41773(Apache 2.4.49 路径穿越)两个变体 —— 朴素编码与双重编码各一次:

    • POST /cgi-bin/.%2e/.%2e/.../bin/sh → 400
    • POST /cgi-bin/%%32%65%%32%65/.../bin/sh → 400
  • PHP-CGI 参数注入(%AD 软连字符 + allow_url_include + auto_prepend_file=php://input)5 条 → 404 / 405

  • CVE-2017-9841(PHPUnit eval-stdin.php 远程代码执行) —— 占绝大多数, 同一个文件名配 几十种路径前缀 轮着试: /vendor/ /lib/ /laravel/ /yii/ /zend/ /ws/ /ws/ec/ /V2/ /tests/ /test/ /testing/ /api/ /demo/ /cms/ /crm/ /admin/ /backup/ /blog/ /workspace/drupal/ …

  • 第 1、2 条就是 CVE-2021-41773(Apache 2.4.49 路径穿越)的两个变体 —— 朴素编码 .%2e 和双重编码 %%32%65。两条都在 nginx 层就被 400 掐掉。

  • 第 3~7 条是 PHP-CGI 参数注入:用软连字符 %AD 绕过简单过滤, 塞 -d allow_url_include=1 -d auto_prepend_file=php://input。 这 5 条正是事件 B 里被 WAF 拦掉的那 5 条。

  • 剩下的绝大多数是 PHPUnit eval-stdin.php(CVE-2017-9841): 同一个文件名配 37 种不同的路径前缀(/vendor/、/lib/、/laravel/、 /yii/、/zend/、/ws/ec/、/V2/、/tests/、/api/、/cms/、/crm/、 /admin/、/backup/、/blog/、/workspace/drupal/ …)。 换个前缀重打一遍是这种扫描器最典型的形态。

  • 最后几条是收尾探测:ThinkPHP invokefunction、PHP pearcmd config-create、 以及一条 /containers/json(探 Docker API 有没有暴露)。

所有请求的 UA 都是 libredtail-http(除前两条 400 的没有 UA)。

这是典型的广撒网式 WebShell 猎手:不针对任何特定站点,把已知的 RCE 路径在所有扫到的 IP 上跑一遍,撞上哪个算哪个。另两个 POST .../bin/sh 的 400 是 nginx 在 URI 解析阶段就拒了,请求没进到应用层。

原来waf还没发力呢


还有一个高手#

103.46.186.148(UA libredtail-http,47 次请求)在跑一套 PHP-CGI 参数注入 / auto_prepend_file 远程代码执行的组合拳:

/hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3d... -> 403 100分 1011
/?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3d... -> 403 100分 1011
/index.php?%25ADd+allow_url_include%3D1+%25ADd+auto_prepend_file%3d... -> 403 100分 1011
/test.hello?%25ADd+allow_url_include%3D1+%25ADd+auto_prepend_file%3d.. -> 403 100分 1011
/index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3d... -> 403 100分 1011

%AD 是软连字符(U+00AD),是这类扫描器用来绕过简单过滤的常见手法; -d allow_url_include=1 -d auto_prepend_file=php://input 是经典的 PHP-CGI RCE 参数。5 条全部 403,命中规则 1011(命令注入)。

同一 IP 还打了两个 LFI 载荷(?lang=../../../../.../usr/...), 得分 70,未过阈值,只记录不拦,后端返回 404。

  • 条数:A = 49,B = 49
  • 请求行完全相同的:49 条,顺序也完全一致
  • 状态码发生变化的:5 条,全在这 5 条上:
    • 第 3 条 404 -> 403 /hello.world?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input
    • 第 4 条 405 -> 403 /?%ADd+allow_url_include%3d1+%ADd+auto_prepend_file%3dphp://input
    • 第 5 条 404 -> 403 /index.php?%25ADd+allow_url_include%3D1+%25ADd+auto_prepend_file%3Dphp://input
    • 第 6 条 404 -> 403 /test.hello?%25ADd+allow_url_include%3D1+%25ADd+auto_prepend_file%3Dphp://inpu
    • 第 7 条 404 -> 403 /index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input

这个结果比「同一种扫描器」强得多 —— 49 条载荷一条不差、顺序一致, 说明是同一个脚本在 12 小时 49 分后原样重放了一遍,只是换了出口 IP。

唯一变化的 5 条状态码都是 404/405 -> 403。其余 44 条状态码完全没变, 因为它们本来就是 404(路径不存在),WAF 只是没拦而已 —— 这两件事要分开看:「没拦」和「拦了」对扫描器来说体验差别只在被识别的那一刻。

WAF 侧的 47 条记录里,命中规则的只有 7 条:

  • 5 条 POST(PHP-CGI 参数注入)→ 100 分,action=block,命中规则 1011(危险协议包装器,匹配到 php://)
  • 2 条 GET(pearcmd 那一对:一条造文件、一条去读它)→ 70 分,action=log,命中规则 1001(路径穿越)
  • 其余 40 条 0 分 pass

也就是说:同一套载荷,WAF 上线前全数畅通,上线后 5 条被拦。

再查一下

反向 DNS : (无 PTR 记录)
APNIC whois:
inetnum : 103.46.186.0 - 103.46.187.255
netname : IDNIC-AIRLINK-ID
descr : PT Air Lintas Komunikasi
country : ID(印度尼西亚)
abuse-mailbox : abuse@airlink.co.id

我想直接真实他们,试试ip溯源#

两个字段的分工#

审计日志里每条记录有两个 IP 相关字段:

  • client —— 真实访客 IP。取值顺序是:先从 X-Real-IP 头取(nginx 的 realip 模块写入),取不到才回退到 TCP 对端。这是报表和溯源要看的那一列

  • peer —— TCP 对端,即 ip:port。因为 WAF 只监听 127.0.0.1,所以这一列永远是 127.0.0.1:<随机端口>,用来和 client 对照判断”这个 client 是不是被可信来源还原出来的”

关于为什么”从 X-Real-IP 取”是安全的:X-Real-IP 是可伪造的头,如果直接信任它,任何人加一个头就能伪装 IP。这里敢用,是因为WAF 只监听回环地址,能连到它的只有本机 nginx;nginx 侧又有 set_real_ip_from 127.0.0.1,只信任回环对端。

信任的是”谁发给我的”(对端在回环),不是”他自称是谁”(头内容)。

验证:外部攻击者能不能被定位#

从本机(公网出口 36.141.24.185)打公网入口 4 条载荷:

/.env -> 403
/.git/config -> 403
/?id=1%20union%20select%201,2,3-- -> 403
/?cmd=;cat%20/etc/passwd -> 403

审计日志里对应 4 条记录,client 全部是 36.141.24.185:

ts=17:01:11
client(真实访客IP)=36.141.24.185 peer(TCP对端)=127.0.0.1:47250
target=/.env act=block score=100 hits=1013,1013 st=403
ts=17:01:11
client(真实访客IP)=36.141.24.185 peer(TCP对端)=127.0.0.1:47262
target=/.git/config act=block score=100 hits=1013,1013 st=403
ts=17:01:11
client(真实访客IP)=36.141.24.185 peer(TCP对端)=127.0.0.1:47278
target=/?id=1%20union%20select... act=block score=100 hits=1020,1020 st=403
ts=17:01:11
client(真实访客IP)=36.141.24.185 peer(TCP对端)=127.0.0.1:47294
target=/?cmd=;cat%20/etc/passwd act=block score=160 hits=1010,1010,1010,1030,1030 st=403

答案:能。 攻击方的真实 IP 被完整记录,且能精确关联到”打了哪个 URL、什么时间、命中了哪条规则、多少分、最终拦还是放”。

当日来源 IP 排行(含自测流量)#

123.207.244.94 128 次 最高分=170 {pass:66, allow:7, log:26, block:29} ← 服务器自身/自测
47.117.95.16 60 次 最高分=0 {pass:60}
36.141.24.209 59 次 最高分=0 {pass:36, allow:23}
127.0.0.1 52 次 最高分=205 {pass:14, block:26, log:5, allow:7} ← 本机直连测试
103.46.186.148 47 次 最高分=100 {block:5, pass:40, log:2} ← 真实攻击者
111.55.211.253 42 次 最高分=0 {pass:3, allow:39}
42.194.192.36 12 次 最高分=0 {allow:12}
183.240.111.153 10 次 最高分=0 {pass:10}
129.204.116.160 10 次 最高分=0 {allow:9, pass:1}
35.241.235.53 9 次 最高分=0 {pass:8, allow:1}

判读要点:

  • 高频但 0 分的 IP(47.117.95.16、36.141.24.209、111.55.211.253) 大概率是正常访客或良性爬虫,不是攻击 —— 别只看次数排序就当攻击者

  • 真正要看的是 最高分 和 block 次数:103.46.186.148 在 WAF 侧 47 条里有 5 条被拦,才是攻击者

  • 注意这张表统计的是 WAF 侧的记录条数。同一个 IP 在公网 nginx 日志里是 49 条 —— 差的那 2 条被 nginx 在请求行解析阶段 400 掉了,根本没走到 WAF

  • 203.0.113.x 这两个 IP 属于 RFC 5737 文档保留段,是自测脚本用的假 IP,不是真实来源

一个数据质量的说明#

日志最开头 4 条(01:06、01:08)的 client 字段是 127.0.0.1:55720 这种”ip”形态、且 peer 为 None。经核对,那是当天更早版本代码留下的历史记录(当时 peer 字段还没实现)。当前版本的 client 恒为纯 IP、peer 恒为127.0.0.1:port,格式是一致且正确的。读日志时要留意跨版本的历史行。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
LYH
LYH,安全方向的在校学生。方向集中在 Web 安全和密码学——Web 这边从 webshell 识别、文件包含到注入类题目都在练,密码学在啃 RSA 和格密码相关的内容,逆向和网络也有涉猎。日常工具链是 Mac + Docker。欢迎交流学习!
公告
欢迎来到我的博客!这里展示我的学习过程与结果。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
站点统计
文章
26
分类
13
标签
19
总字数
180,600
运行时长
0 天
最后活动
0 天前

文章目录