Coordinated Disclosure Timeline

Summary

SRS’s api-server server is vulnerable to a drive-by command injection.

Product

SRS

Tested Version

v4.0-r4

Details

Issue: Command injection (GHSL-2023-025)

The api-server server is vulnerable to command injection. An attacker may send a request to the /api/v1/snapshots endpoint containing any commands to be executed as part of the body of the POST request. The handler for the /api/v1/snapshots endpoint is:

	http.HandleFunc("/api/v1/snapshots", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != "POST" {
			SrsWriteDataResponse(w, struct{}{})
			return
		}

		if err := func() error {
			body, err := ioutil.ReadAll(r.Body)
			if err != nil {
				return fmt.Errorf("read request body, err %v", err)
			}
			log.Println(fmt.Sprintf("post to snapshots, req=%v", string(body)))

			msg := &SrsSnapShotRequest{}
			if err := json.Unmarshal(body, msg); err != nil {                           // [1]
				return fmt.Errorf("parse message from %v, err %v", string(body), err)
			}
			log.Println(fmt.Sprintf("Got %v", msg.String()))

			if msg.IsOnPublish() {
				sw.Create(msg)                                                          // [2]
			} else if msg.IsOnUnPublish() {
				sw.Destroy(msg)
			} else {
				return fmt.Errorf("invalid message %v", msg.String())
			}

			SrsWriteDataResponse(w, &SrsCommonResponse{Code: 0})
			return nil
		}(); err != nil {
			SrsWriteErrorResponse(w, err)
		}
	})

The body of the POST request is unmarshalled in [1] and passed to the sw.Create method. The Create method will craft a RTMP URL using untrusted data (e.g.: the app value from the JSON request):

func (v *SnapshotWorker) Create(sm *SrsSnapShotRequest) {
	streamUrl := fmt.Sprintf("rtmp://127.0.0.1/%v/%v?vhost=%v", sm.App, sm.Stream, sm.Vhost)
	if _, ok := v.snapshots.Load(streamUrl); ok {
		return
	}
	sj := NewSnapshotJob()
	sj.SrsSnapShotRequest = *sm
	sj.updatedAt = time.Now()
	go sj.Serve(v.ffmpegPath, streamUrl) // [3]
	v.snapshots.Store(streamUrl, sj)
}

The streamUrl is then passed to the SnapshotJob.Serve() method which, in turn, passes it to the SnapshotWorker.do() method where the inputUrl is interpolated into the param variable which is later executed as a shell command:

	param := fmt.Sprintf("%v -i %v -vf fps=1 -vcodec png -f image2 -an -y -vframes %v -y %v", ffmpegPath, inputUrl, v.vframes, normalPicPath)
	log.Println(fmt.Sprintf("start snapshot, cmd param=%v", param))
	timeoutCtx, _ := context.WithTimeout(v.cancelCtx, v.timeout)
	cmd := exec.CommandContext(timeoutCtx, "/bin/bash", "-c", param)

This issue was found by the Command built from user-controlled sources CodeQL query.

Impact

This issue may lead to Remote Code Execution (RCE).

CVE

Resources

Credit

This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz), using the Command Injection CodeQL query.

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-025 in any communication regarding this issue.