[Go语言]SSTI从0到1

news/2024/5/19 23:20:41 标签: golang, 开发语言, web安全, CTF

[Go语言]SSTI从0到1

  • 1.Go-web基础及示例
  • 2.参数处理
  • 3.模版引擎
    • 3.1 text/template
    • 3.2 SSTI
  • 4.[LineCTF2022]gotm
    • 1.题目源码
    • 2.WP

1.Go-web基础及示例

package main
import (
	"fmt"
	"net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) { // 定义一个
	fmt.Fprintln(w, "hello world!")
}
func main() {
	http.HandleFunc("/hello", sayHello)  //浏览器访问/hello,将会转到sayHello去处理
	http.ListenAndServe(":8888", nil) // 监听端口并处理请求
}

其中 “net/http” 是Go自带的实现web服务的模块,可以处理http请求,其中http.HandleFunc是用来绑定路由函数,相当于python的flask的app.route函数

然后就是路由函数sayHello了,这个函数接受两个参数,其中 w 是web接口参数,该变量可发送到前端web页面,r 是http报文参数,用于存放http请求的UA头,表单数据等信息

然后 fmt.Fprintln 函数是用于输出在器(io.Writer)中例如web接口

这里输出 “hello world”,如图所示

在这里插入图片描述

2.参数处理

我们将sayHello函数改动如下

func sayHello(w http.ResponseWriter, r *http.Request) {
  var name = r.FormValue("name")
  var para = ("hello," + name + "!")
  fmt.Fprintln(w, para)
}

r.FormValue为接收表单数据的数组,包括GET与POST

处理表单请求的方法如下

1.直接获得

PostFormValue或FormValue方法

前者只能解析Post请求

2.ParseForm()方法间接获得

r.ParseForm()  解析请求的主体,化为键值对组合

键值对会被存储在 r.Form 和 r.PostForm 字段中

  • r.Form 是一个 url.Values 类型,它表示 URL 查询参数和 POST 表单字段的集合。可以通过 r.Form.Get(key)、r.Form[key] 或 r.FormValue(key) 方法来获取表单字段的值。
  • r.PostForm 是一个 url.Values 类型,它只包含 POST 表单字段的数据。与 r.Form 不同的是,r.PostForm 不会自动解析 URL 查询参数或其他非 POST 提交的数据。

示例如下:

3.模版引擎

GO语言提供了两个模板包,一个是 html/template 模块,另一个是 text/template 模块,其中text/template模板引擎与Go语言的SSTI有关

3.1 text/template

示例:

package main

import (
    "fmt"
    "net/http"
    "strings"
    "text/template"
)

type User struct {
    Id     int
    Name   string
    Passwd string
}

func StringTplExam(w http.ResponseWriter, r *http.Request) {
    user := &User{1, "admin", "123456"}
    r.ParseForm()
    arg := strings.Join(r.PostForm["name"], "")
    tpl1 := fmt.Sprintf(`<h1>Hi, ` + arg + `</h1> Your name is ` + arg + `!`)
    html, err := template.New("login").Parse(tpl1)
    html = template.Must(html, err)
    html.Execute(w, user)
}

func main() {
    server := http.Server{
        Addr: "127.0.0.1:8080",
    }
    http.HandleFunc("/login", StringTplExam)
    server.ListenAndServe()
}

首先讲解一下这段代码,它使用了 “text/template” 模板引擎,该模板引擎并没有对传入的数据进行html编码,可导致SSTi,然后定义了一个User结构体,用于给模版传参

type User struct {
    Id     int
    Name   string
    Passwd string
}

然后通过fmt.Sprintf函数将传入的name参数拼接到模版当中,再通过以下语句,利用template.New(“login”)创建了一个名为 login 的模板,然后使用 Parse(tpl1) 方法,被作为模版的字符串存储到login模板中

 html, err := template.New("login").Parse(tpl1)

其中html就代表刚才创建的login模板,然后err代表创建模板过程中的报错信息(一般不会有),然后通过以下语句判断模板是否创建成功,若创建成功则继续运行,失败则退出程序并报错

html = template.Must(html, err)

最后通过 Execute 方法进行模版翻译,将user结构体中的值翻译到html的模板中,然后输出到w接口,也就是web前端

html.Execute(w, user)

3.2 SSTI

我们以上面的web源码为例

首先访问 login 路由,然后POST传入一个参数,可以看到回显正常
在这里插入图片描述

我们再传入 name={{.Name}},其中双重大括号为该模板引擎的占位符,可以被翻译,我们使用 {{.参数名}} 的格式,可以访问构建模版时所传入的参数

type User struct {
    Id     int
    Name   string
    Passwd string
}

user := &User{1, "admin", "123456"}

如图所示
在这里插入图片描述

CTF2022gotm_164">4.[LineCTF2022]gotm

1.题目源码

main.go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"text/template"

	"github.com/golang-jwt/jwt"
)

type Account struct {
	id         string
	pw         string
	is_admin   bool
	secret_key string
}

type AccountClaims struct {
	Id       string `json:"id"`
	Is_admin bool   `json:"is_admin"`
	jwt.StandardClaims
}

type Resp struct {
	Status bool   `json:"status"`
	Msg    string `json:"msg"`
}

type TokenResp struct {
	Status bool   `json:"status"`
	Token  string `json:"token"`
}

var acc []Account
var secret_key = os.Getenv("KEY")
var flag = os.Getenv("FLAG")
var admin_id = os.Getenv("ADMIN_ID")
var admin_pw = os.Getenv("ADMIN_PW")

func clear_account() {
	acc = acc[:1]
}

func get_account(uid string) Account {
	for i := range acc {
		if acc[i].id == uid {
			return acc[i]
		}
	}
	return Account{}
}

func jwt_encode(id string, is_admin bool) (string, error) {
	claims := AccountClaims{
		id, is_admin, jwt.StandardClaims{},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(secret_key))
}

func jwt_decode(s string) (string, bool) {
	token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) {
		return []byte(secret_key), nil
	})
	if err != nil {
		fmt.Println(err)
		return "", false
	}
	if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid {
		return claims.Id, claims.Is_admin
	}
	return "", false
}

func auth_handler(w http.ResponseWriter, r *http.Request) {
	uid := r.FormValue("id")
	upw := r.FormValue("pw")
	if uid == "" || upw == "" {
		return
	}
	if len(acc) > 1024 {
		clear_account()
	}
	user_acc := get_account(uid)
	if user_acc.id != "" && user_acc.pw == upw {
		token, err := jwt_encode(user_acc.id, user_acc.is_admin)
		if err != nil {
			return
		}
		p := TokenResp{true, token}
		res, err := json.Marshal(p)
		if err != nil {
		}
		w.Write(res)
		return
	}
	w.WriteHeader(http.StatusForbidden)
	return
}
func regist_handler(w http.ResponseWriter, r *http.Request) {
	uid := r.FormValue("id")
	upw := r.FormValue("pw")

	if uid == "" || upw == "" {
		return
	}
	if get_account(uid).id != "" {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	if len(acc) > 4 {
		clear_account()
	}
	new_acc := Account{uid, upw, false, secret_key}
	acc = append(acc, new_acc)
	p := Resp{true, ""}
	res, err := json.Marshal(p)
	if err != nil {
	}
	w.Write(res)
	return
}
func flag_handler(w http.ResponseWriter, r *http.Request) {
	token := r.Header.Get("X-Token")
	if token != "" {
		id, is_admin := jwt_decode(token)
		if is_admin == true {
			p := Resp{true, "Hi " + id + ", flag is " + flag}
			res, err := json.Marshal(p)
			if err != nil {
			}
			w.Write(res)
			return
		} else {
			w.WriteHeader(http.StatusForbidden)
			return
		}
	}
}
func root_handler(w http.ResponseWriter, r *http.Request) {
	token := r.Header.Get("X-Token")
	if token != "" {
		id, _ := jwt_decode(token)
		acc := get_account(id)
		tpl, err := template.New("").Parse("Logged in as " + acc.id)
		if err != nil {
		}
		tpl.Execute(w, &acc)
	} else {

		return
	}
}
func main() {
	admin := Account{admin_id, admin_pw, true, secret_key}
	acc = append(acc, admin)
	http.HandleFunc("/", root_handler)
	http.HandleFunc("/auth", auth_handler)
	http.HandleFunc("/flag", flag_handler)
	http.HandleFunc("/regist", regist_handler)
	log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil))
}

2.WP

审计代码可得该web网站共有以下路由

  http.HandleFunc("/", root_handler) //模板

  http.HandleFunc("/auth", auth_handler)  //登陆

  http.HandleFunc("/flag", flag_handler)

  http.HandleFunc("/regist", regist_handler)  //注册

先分析下flag路由

id, is_admin := jwt_decode(token)
		if is_admin == true {
			p := Resp{true, "Hi " + id + ", flag is " + flag}
			res, err := json.Marshal(p)
			if err != nil {
			}

发现需要进行jwt伪造,让 is_admin 的值为ture

访问regist路由注册账号,形成原始jwt

在这里插入图片描述

访问auth路由,登陆刚才注册的账号,得到一段jwt加密的字符串

在这里插入图片描述

对其解密得,is_admin的值默认为false

在这里插入图片描述

由此可得我们只需添加 X-Token 的请求头即可,内容为修改后的jwt字符串,接下来我们需要通过根路由进行SSTI,得到jwt加密的密钥,然后进行伪造

token := r.Header.Get("X-Token")

分析根路由可得,注入点在jwt加密数据的id部分

if token != "" {
		id, _ := jwt_decode(token)
		acc := get_account(id)
		tpl, err := template.New("").Parse("Logged in as " + acc.id)
		if err != nil {
		}
		tpl.Execute(w, &acc)
	} 

我们再次访问regist路由,发送以下payload,来获得acc结构体的所有信息

regist?id={{.}}&pw=123
//不能使用{{.secret_key}}注入得到key字段,因为root_handler函数中得到的acc是数组中的地址,也就是get_account函数通过在全局变量acc数组中查找我们的用户,这种情况下直接注入{{.secret_key}}会返回空

访问author路由,得到SSTI的密文如下

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss

再次访问根路由,并添加 X-Token 请求头,值为刚才的jwt密文,得到jwt加密密钥(this_is_f4Ke_key)

在这里插入图片描述

回到 jwt.io网站,将is_admin的值设为true,并加密

在这里插入图片描述

得到密文

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo

访问flag路由添加X-Token请求头

在这里插入图片描述

成功得到flag


http://www.niftyadmin.cn/n/5172882.html

相关文章

Windows7+vs2005源码安装subversion

Windows源码安装subversion 一、运行环境 windows7 32位系统 VS2005完整安装 二、源码编译环境配置 1、python环境安装 python-2.4.msi2、perl环境安装 ActivePerl-5.8.8.822-MSWin32-x86-280952.msi3、openssl编译 C:>cd openssl-0.9.7f C:>perl Configure VC-W…

MATLAB的编程与应用,匿名函数、嵌套函数、蒙特卡洛法的掌握与使用

目录 1.匿名函数 1.1.匿名函数的定义与分类 1.2.匿名函数在积分和优化中应用 2.嵌套函数 2.1.嵌套函数的定义与分类 2.2.嵌套函数彼此调用关系 2.3.嵌套函数在积分和微分中应用 3.微分和积分 4.蒙特卡洛法 4.1.圆周率的模拟 4.2.计算N重积分&#xff08;均匀分布&am…

C语言--前置++与后置++

:自增1 注意区分前置和后置 前置&#xff1a;先&#xff0c;后使用 后置&#xff1a;先使用&#xff0c;后 --:自减1 注意区分前置和后置 前置&#xff1a;先-- &#xff0c;后使用 后置&#xff0c;先使用&#xff0c;后-- int main() {int i 10;//int j i;//前置,先…

python:通过help函数来查看帮助信息(查看类的对象的行为)

python&#xff1a;通过help函数来查看帮助信息 文章目录 python&#xff1a;通过help函数来查看帮助信息实例查看list类的一些对象的行为具体查看list类的对象的行为 这些是这些行为的使用描述 实例 查看list类的一些对象的行为 help(list)# 输出内容 Help on class list in…

ubuntu cp210x(USB转串口)驱动安装教程

ubuntu cp210x&#xff08;USB转串口&#xff09;驱动安装 前言1. 驱动下载2. 驱动编译和安装3. 可能遇到的问题及解决办法3.1 第一种解决方案&#xff1a;检查当前是否已有CP210x.ko驱动3.2 第二种解决方案&#xff1a;修改驱动文件&#xff0c;编译 前言 CP2108 是一款高度集…

代码随想录训练营Day5:哈希数组

算是哈希的容器&#xff1a;数组&#xff08;适合连续存放&#xff09;&#xff1b;set&#xff0c;map&#xff08;适合无序存放&#xff09;。所以数组操作就是hash[i];而set,map.insert(元素)&#xff0c;map可以map[]是因为map存放了键值对可以索引查找。关于几个数组相加等…

计蒜客详解合集(3)期

目录 T1236——分苹果 T1113——整理药名 T1153——整数奇偶排列 T1249——漂亮的字符串 T1168——统计素数个数 T1160——甲流病人筛选 T1236——分苹果 分享一道特别简单的题。 蒜头君要把一堆苹果分给 个小朋友&#xff0c;要使每个人都能拿到苹果&#xff0c;而目每…

win10网络和Internet设置

win10网络设置 win10进入网络设置的常用入口有两个 第一个入口 桌面右下角右键网络图标&#xff0c;然后打开“网络和Internt设置” 第二个入口 桌面的“我的网络”快捷方式&#xff0c;或者我的电脑进去后&#xff0c;左侧栏找到“网络” 右键“属性” 可以看到&#xff0c;…